10.1 引言
- 本章重点在于类的设计,以及探讨面向过程编程和面向对象编程的不同。
- 前面章节介绍了对象和类。我们也学习了如何定义类、创建对象以及使用对象。本书的方法是在教授面向对象程序设计之前,先讲述问题求解和基本程序设计技术。本章将给出面向过程和面向对象程序设计的不同之处,你将会看到面向对象程序设计的优点,并学习如何高效地使用它。
- 这里,我们的焦点放在类的设计上。我们将使用几个例子来诠释面向对象方法的优点。这些例子包括如何在应用程序中设计新的类、如何使用这些类,以及介绍Java API 中的一些新的类。
10.2 类的抽象和封装
- 类的抽象是指将类的实现和类的使用分离开,实现的细节被封装并且对用户隐藏,这被称为类的封装
- Java提供了多层次抽象。类抽象(class abstraction)是将类的实现和使用分离。类的创建者描述类的功能,让使用者明白如何使用类。从类外可以访问的public 构造方法、普通方法和数据域的集合以及对这些成员预期行为的描述,构成了类的合约(class‘s contract)。如图10-1所示,类的使用者不需要知道类是如何实现的。实现的细节通过封装对用户隐藏起来,这称为类的封装(class encapsulation)。例如:可以创建一个Circle对象,并且可以在不知道面积是如何计算出来的情况下,求出这个圆的面积。由于这个原因,类也称为抽象数据类型(Abstract Data Type, ADT)。
- 类抽象和封装是一个问题的两个方面。现实生活中的许多例子都可以说明类抽象的概念。例如:考虑建立一个计算机系统。个人计算机有很多组件 —- CPU、内存、磁盘、主板和风扇等。每个组件都可以看作是一个有属性和方法的对象。要使各个组件一起工作,只需要知道每个组件是指怎么用的以及是如何与其他组件进行交互的,而无须了解这些组件内部是如何工作的。内部功能的实现被封装起来,对你是隐藏的。所以,你可以组装一台计算机,而不需要了解每个组件的功能是如何实现的。
- 对计算机系统的这种模拟准确地反映了面向对象方法。每个组件都可以看成组件类的对象。例如,你可能已经建立了一个类,模拟计算机上的各种类型的风扇,它具有风扇尺寸和速度等属性,以及像打开和停止这样的方法。一个具体的风扇就是该类具有特定属性值的实例。
- 将得到一笔贷款作为另一个例子。一笔具体的贷款可以看作贷款类Loan的一个对象,利率、贷款额以及还贷周期都是它的数据属性,计算每月偿还额和总偿还额是它的方法。当你购买一辆汽车时,就用贷款利率、贷款额和还贷周期实例化这个类,创建一个贷款对象。然后,就可以使用这些方法计算贷款的月偿还额和总偿还额。作为一个贷款类Loan的用户,是不需要知道这些方法是如何实现的
- 程序清单2-9给出计算贷款偿还额的程序。这个程序不能再其他程序中重用,因为计算支付的代码仿放在main方法中。解决这个问题的一种方式就是定义计算月偿还额和总偿还额的
静态方法
。但是,这个解决方案是有局限性的。假设希望将一个日期和这个贷款联系起来。如果不使用对象的话,没有一个好的办法可以将一个日期和贷款联系起来。传统的面向过程式编程是动作驱动的,数据和动作是分离的。面向对象编程的范式重点在于对象,动作和数据一起定义在对象中。为了将日期和贷款联系起来,可以定义一个贷款类,将日期和贷款的其他属性一起作为数据域,并且贷款数据和动作集成在一个对象中。图10-2 给出了Loan类的UML类图。 - 将图10-2的UML图看作Loan类的合约。贯穿本书,你将扮演两个角色,一个是类的用户,一个是类的开发者。记住用户可以在不知道类是如何实现的情况下使用类。
- 假设Loan类是可用的。程序清单10-1中的程序使用该类。
程序清单10-1 TestLoanClass.java
import java.util.Scanner;
public class TestLoanClass {
/**
* Main method
*/
public static void main(String[] args) {
//Create a Scanner
Scanner input = new Scanner(System.in);
//Enter annual interest rate
System.out.print("Enter annual interest rate, for example, 8.25: ");
double annualInterestRate = input.nextDouble();
//Enter number of years
System.out.print("Enter number of years as an integer: ");
int numberOfYears = input.nextInt();
//Enter loan amount
System.out.print("Enter loan amount, for example , 120000.95");
double loanAmount = input.nextDouble();
//Create a Loan object
Loan loan = new Loan(annualInterestRate, numberOfYears, loanAmount);
//Display loan date, monthly payment , and total payment
System.out.printf("The loan was created on %s\n" +
"The monthly payment is %.2f\n The total payment is %.2f\n",
loan.getLoanDate().toString(), loan.getMonthlyPayment(), loan.getTotalPayment());
}
}
- main方法读取利率和还贷周期(以年为单位)以及贷款额度,创建一个Loan对象,然后使用Loan类中的实例方法获取月偿还额和总偿还额。
- Loan类可以如程序清单10-2实现。
程序清单10-2 Loan.java
public class Loan {
private double annualInterestRate;
private int numberOfYears;
private double loanAmount;
private java.util.Date loanDate;
/**
* Default constructor
*/
public Loan() {
this(2.5, 1, 1000);
}
/**
* Construct a loan with specified annual interest rate,
* number of years, and loan amount
*/
public Loan(double annualInterestRate, int numberOfYears, double loanAmount) {
this.annualInterestRate = annualInterestRate;
this.numberOfYears = numberOfYears;
this.loanAmount = loanAmount;
loanDate = new java.util.Date();
}
/**
* Return annualInterestRate
*/
public double getAnnualInterestRate() {
return annualInterestRate;
}
/**
* Set a new annualInterestRate
*/
public void setAnnualInterestRate(double annualInterestRate) {
this.annualInterestRate = annualInterestRate;
}
/**
* Return numberOfYears
*/
public int getNumberOfYears() {
return numberOfYears;
}
/**
* Set a new numberOfYears
*/
public void setNumberOfYears(int numberOfYears) {
this.numberOfYears = numberOfYears;
}
/**
* Return loanAmount
*/
public double getLoanAmount() {
return loanAmount;
}
/**
* Set a new loanAmount
*/
public void setLoanAmount(double loanAmount) {
this.loanAmount = loanAmount;
}
/**
* Find monthly payment
*/
public double getMonthlyPayment() {
double monthlyInterestRate = annualInterestRate / 1200;
double monthlyPayment = loanAmount * monthlyInterestRate /
(1 - (1 / Math.pow(1 + monthlyInterestRate, numberOfYears * 12)));
return monthlyPayment;
}
/**
* Find total payment
*/
public double getTotalPayment() {
double totalPayment = getMonthlyPayment() * numberOfYears * 12;
return totalPayment;
}
/**
* Return loan data
*/
public java.util.Date getLoanDate() {
return loanDate;
}
}
- 从类开发者的角度来看,设计类是为了让很多不同的用户可以使用。为了在更大的应用范围内使用类,类应该支持通过构造方法、属性和放方法提供各种方式的定制
- Loan类包含两个构造方法、四个获取方法、三个设置方法,以及求月偿还额和总偿还额的方法。可以通过使用无参构造方法或者带三个参数(年利率、年数和贷款额)的构造方法来构造一个Loan对象。当创建一个贷款对象时,它的日期存储在loanDate域中,getLoanDate方法返回日期。方法getAnnualInterest、getNumberOfYears和getLoanAmount分别返回年利率、还款年数以及贷款总额。这个类的所有数据属性和方法都被绑定到Loan类的某个特定实例上。因此,它们都是实力变量和实例方法。
- Loan类的UML图如图10-2所示,使用该图编写使用Loan类的测试程序,即使不知道这个Loan类是如何执行的。这有三个优点:
- 揭示了开发类和使用类是两个不同的任务
- 能使你跳过某个类的复杂实现,而不打乱整本书的顺序
- 如果通过使用它熟悉了该类,那么你将更容易学会如何实现这个类
- 从现在开始的所有例子,在将注意力放在它的实现上之前,你都可以先在这个类中创建一个对象,并且尝试使用它的方法
10.3 面向对象的思想
- 面向过程的范式重点在于设计方法。面向对象的范式将数据和方法耦合在一起构成对象。使用面向对象范式的软件设计重点在于对象以及对象上的操作。
- 第1~8章介绍使用循环、方法和数组来解决问题的基本程序设计技术。这些技术的学习为面向对象程序设计打下了坚实的基础。类为构建可重用软件提供了更好的灵活性和模块化。本节使用面向对象方法来改进第3章中介绍的一个问题的解决方案。在这个改进的过程中,可以洞察面向过程程序设计和面向对象程序设计的不同,也可以看出使用对象和类来开发可重用代码的优势
- 程序清单3-4 给出了计算身体质量指数(BMI)的程序。因为它的代码在main方法中,所以不能在其他程序中重用。为使之具备可重用性,可以定义一个静态方计算身体质量指数,如下所示:
public static double getBMI(double weight, double height)
- 这个方法对于计算给定体重和身高的身体质量指数是有用的。但是,它是有局限性的。假设需要将体重和身高同一个人的名字与出生日期相关联,虽然可以分别声明几个变量来存储这些值,但是这些值不是紧密耦合在一起的。将它们耦合在一起的理想方法就是创建一个将它们全部包含的对象。因为这些值都被绑定到单独的对象上,所以它们应该存储在实例数据域中。可以定义一个名为BMI的类,如图10-3所示。
- 假设BMI类是可用的。程序清单10-3给出使用这个类的测试程序。
程序清单10-3 UseBMIClass.java
public class UseBMIClass {
public static void main(String[] args) {
BMI bmi1 = new BMI("Kim Yang", 18 , 145,70);
System.out.println("The BMI for " + bmi1.getName() + " is "
+ bmi1.getBMI() + " " + bmi1.getStatus());
BMI bmi2 = new BMI("Susan King", 215, 70);
System.out.println("The BMI for " + bmi2.getName() + " is "
+ bmi2.getBMI() + " " + bmi2.getStatus());
}
}
- 第3行为Kim Yang 创建了一个对象bmi1,第7行为Susan King创建一个对象bmi2。可以使用实例方法getName( )、getBMI( )和getStatus( ) 返回一个BMI 对象中的BMI信息。
- BMI类可以如程序清单10-4实现
程序清单10-4 BMI.java
public class BMI {
private String name;
private int age;
//in pounds
private double weight;
//in inches
private double height;
public static final double KILOGRAMS_PER_POUND = 0.45359237;
public static final double METERS_PER_INCH = 0.0254;
public BMI(String name, int age, double weight, double height){
this.name = name;
this.age = age;
this.weight = weight;
this.height = height;
}
public BMI(String name , double weight, double height){
this(name, 20, weight , height);
}
public double getBMI(){
double bmi = weight * KILOGRAMS_PER_POUND / ((height * METERS_PER_INCH) * (height * METERS_PER_INCH));
return Math.round(bmi * 100) / 100.0;
}
public String getStatus(){
double bmi = getBMI();
if (bmi < 18.5){
return "Underweight";
} else if (bmi < 25) {
return "Normal";
} else if (bmi < 30) {
return "Overweight";
}else {
return "Obese";
}
}
public String getName(){
return name;
}
public int getAge() {
return age;
}
public double getWeight() {
return weight;
}
public double getHeight() {
return height;
}
}
- 使用体重和身高来计算BMI的数学公式已经在3.8节中给出。实例方法getBMI( ) 返回BMI。因为体重和身高是对象的实例数据域,getBMI( ) 方法可以使用这些属性来计算对象的BMI值
- 实例方法getStatus( ) 返回解释BMI的字符串。这个解释也已经在3.8节中给出
- 这个例子演示了面向对象范式比面向过程范式有优势的地方。面向过程范式重在设计方法。面向对象范式将数据和方法都结合在对象中。使用面向对象范式的软件设计重在对象和对象上的操作。面向对象方法结合了面向过程范式的功能以及将数据和操作集成在对象中的特性。
- 在面向过程程序设计中,数据和数据上的操作是分离的,而且这种做法要求传递数据给方法。面向对象程序设计将数据和对它们的操作都放在一个对象中。这个方法解决了很多面向过程程序设计的固有问题。面向对象程序设计方法以一种反映真实世界的方式组织程序,在真实世界中,所有的对象都和属性及动作相关联。使用对象提高了软件的可重用性,并且是程序更易于开发和维护。Java程序设计设计对象这个术语的思想,一个Java程序可以看作是一个相互操作的对象集合。
10.4 类的关系
- 为了设计类,需要探究类之间的关系。类之间的关系通常有关联、聚合、组合以及继承。
- 本节探讨关联、聚合以及组合关系。继承关系将在下一章中介绍。
10.4.1 关联
- 关联是一种常见的二元关系,描述两个类之间的活动。例如,学生选取课程是Student类和Course类之间的一种关联,而教师教授课程是Faculty 类和Course 类之间的关联。这些关联可以使用UML图形标识来表达,如图10-4所示
- 关联由两个类之间的实线表示,可以有一个可选的标签描述关系。图10-4中,标签是Take和Teach。每个关系可以有一个可选的小的黑色三角形表明关系的方向。在该图中,—>表明学生选取课程(而不是相反方向的课程选取学生)。
- 关系中涉及的每个类可以有一个角色名称,描述类在该关系中担当的角色。图10-4 中,Teacher是Faculty 的角色名。
- 关联中涉及的每个类可以给定一个多样性(multiplicity),放置在类的边上用于给定UML 图中关系所涉及的类的对象数。多样性可以是一个数字或一个区间,决定在关系中涉及该类的对象数。字符 * 意味着无数多个对象,而m … n表示对象数位于m 和 n 之间,并且包括 m 和 n。图10-4中,每个学生可以选取任意数量的课程数,每门课程可以有至少5 个最多60个学生。每门课程只由一位教师教授,并且每位教师可以教授0到3门课程。
- 在Java代码中,可以通过使用数据域以及方法来实现关联。例如:图10-4中的关系可以使用图10-5中的类来实现。“一个学生选取一门课程”关系使用Student类中的addCourse方法和Course类中的addStudent 方法实现。关系”一位教师教授一门课程”使用Faculty 类中的addCourse方法和Course 类中的setFaculty 方法实现。Student 类可以使用一个列表来存储学生选取的课程,Faculty 类可以使用一个列表来存储教师教授的课程,Course 类可以使用一个列表来存储课程中登记的学生以及一个数据域来存储教授该课程的教师。
- 可以有很多种可能的方法实现类之间的关系。例如,Course类中的学生和教师信息可以省略,因为它们已经在Student 和 Faculty 类中了。同样的,如果不需要知道一个学生选取的课程或者教师教授的课程,Student 或者Faculty 类中的数据域courseList和方法 addCourse 也可以省略。
10.4.2 聚集和组合
- 聚集是关联的一种特殊形式,代表了两个对象之间的归属关系。聚集对has - a 关系进行建模。所有者对象称为聚集对象,它的类称为聚集类。而从属对象称为被聚集对象,它的类称为被聚集类。
- 如果被聚集对象的存在依赖于聚集对象,我们称这个两个对象之间的关系为组合(composition)。换句话说,被聚集对象不能单独存在。例如:“一个学生有一个名字”就是学生类Student 与名字类Name 之间的一个组合关系,因为Name 依赖于 Student ; 而“一个学生有一个地址”是学生类Student 与地址类Address 之间的一个聚集关系,因为一个地址自身可以单独存在。组合暗示了独占性的拥有。一个对象拥有另外一个对象。当拥有者对象销毁了,依赖对象也会销毁。在UML中,附加在聚集类上的实心菱形表示它和被聚集类之间具有组合关系;而附加在聚集类上的空心菱形表示它与被聚集类之间具有聚集关系,如图10-6所示
- 在图10-6中,每个学生只能有一个地址,而每个地址最多可以被3个学生共享。每个学生都有一个名字,而每个学生的名字都是唯一的。
- 聚集关系通常被表示为聚集类中的一个数据域。例如:图10-6 中的关系可以使用图10-7 中的类来实现。关系“一个学生拥有一个名字”以及“一个学生有一个地址”在Student 类中的数据域name和address中实现。
- 聚集可以存在于同一类的对象之间。例如:一个人可能有一个管理者,如图10-8所示。
- 在关系“一个人有一个管理者”中,管理者可以如下表示为Person类的一个数据域:
public class Person(){
//The type of the data is the class itself
private Person supervisor;
...
}
- 如果一个人可以有几个管理者,如图10-9a所示,可以用一个数组存储管理者,如图10-9b所示。
- 由于聚集和组合关系都以同样的方式用类来表示,为了简单起见,我们不区分它们,将两者都称为组合。
10.5 示例学习:涉及Course类
- 本节设计一个类来对课程建模
- 本书的宗旨是“通过例子来教学,通过动手来学习(teaching by example and learning by doing)”。本书提供了各种例子来演示面向对象程序设计。本节以及下一节将给出设计类的补充示例。
- 假设需要处理课程信息。每门课程都有一个名称以及选课的学生,要能够向 / 从这个课程添加 / 删除一个学生。可以使用一个类来对课程建模,如图10-10所示
- 可以向构造方法Course(String name)传递一门课程的名称来创建一个Course对象。可以使用addStudent(String student) 方法向某门课程添加学生,使用dropStudent(String student)方法从某门课程中删除一个学生,使用getStudents( )方法可以返回选这门课程的所有学生。假设Course 类是可用的。程序清单10-5 给出了一个测试类,这个测试类创建了两门课程,并向课程中添加学生。
程序清单 10-5 TestCourse.java
public class TestCourse {
public static void main(String[] args) {
Course course1 = new Course("Data Structures");
Course course2 = new Course("Database Systems");
course1.addStudent("Peter Jones");
course1.addStudent("Kim Smith");
course1.addStudent("Anne Kennedy");
course2.addStudent("Peter Jones");
course2.addStudent("Steve Smith");
System.out.println("Number of students in course1: "
+ course1.getNumberOfStudents());
String[] students = course1.getStudents();
for (int i = 0; i < course1.getNumberOfStudents(); i++) {
System.out.print(students[i] + ", ");
}
System.out.println();
System.out.print("Number of students in course2: "
+ course2.getNumberOfStudents());
}
}
- Course类在程序清单10-6 中实现。它使用一个数组存储选该门课的学生。为简单起见,假设选课的人数最多为100 。在第3行使用new String[100]创建数组。addStudent方法向这个数组中添加学生。只要有新的学生加入课程,numberOfStudents就增加1。getStudents方法返回这个数组。dropStudent方法留作练习。
程序清单 10-6 Course.java
public class Course {
private String courseName;
private String[] students = new String[100];
private int numberOfStudents;
public Course(String courseName) {
this.courseName = courseName;
}
public void addStudent(String student) {
students[numberOfStudents] = student;
numberOfStudents++;
}
public String[] getStudents() {
return students;
}
public int getNumberOfStudents() {
return numberOfStudents;
}
public String getCourseName() {
return courseName;
}
public void dropStudent(String student) {
//Left as exercise in Programing Exercise 10.9
}
}
- 数组的大小固定为100,所以在一门课程中不能有多于100个学生。可以在编程练习题10.9中改进它,是数组尺寸可以自动增加。
- 创建一个Course对象时就创建了一个数组对象。Course对象包含对数组的引用。简洁起见,可以说Course对象包含了该数组
- 用户可以创建一个Course对象,然后通过公有方法addStudent、dropStudent、getNumberOfStudents和getStudents来操作它。然而,用户不需要知道这些方法是如何实现的。Course类封装了内部的实现。该例是使用一个数组存储学生的,但也可以使用不同的数据结构存储students。只要公共方法的合约保持不变,那么使用Course类的程序也无须修改。
10.6 示例学习:设计栈类
- 本节设计一个类对栈建模
- 回顾一下,栈(stack)是一种”后进后出“的方式存放数据的数据结构,如图10-11所示。
- 栈有很多应用。例如:编译器使用栈来处理方法的调用。当调用某个方法时,方法的参数和局部变量都被压入栈中。当一个方法调用另一个方法时,新方法的参数和局部变量被压入栈中。当方法运行完返回它的调用者时,从栈中释放与它相关的空间。
- 可以定义一个类建模栈。为简单起见,假设该栈存储int数值。因此,命名这个栈类为StackOfIntegers。这个类的UML图如图10-12所示。
- 假设该类是可用的。程序清单10-7中的测试程序使用该类创建一个栈,其中存储了10个整数0,1,2,… ,9,然后按逆序显示它们
程序清单 10-7 TestStackOfIntegers.java
public class TestStackOfIntegers {
public static void main(String[] args) {
StackOfIntegers stack = new StackOfIntegers();
for (int i = 0; i < 10; i++) {
stack.push(i);
}
while (!stack.empty()) {
System.out.print(stack.pop() + " ");
}
}
}
- 如何实现StackOfInteger类呢?栈中的元素都存储在一个名为elements 的数组中。创建一个栈的时候,同时也创建了这个数组。类的无参构造方法创建一个默认容量为16的数组。变量size记录了栈中元素的个数。类的无参构造方法创建一个默认容量为16的数组。变量size记录了栈中元素的个数,而size - 1 是栈顶元素的下标,如图10-13所示。对空栈来说,size为0。
- StackOfIntegers 类在程序清单10-8中实现。方法empty( )、peek( )、pop( ) 和getSize( )都容易实现。为了实现方法push(int value),如果size < capacity ,则将value赋值给elements[size]。如果栈已满(即size >= capacity), 则创建一个容量为当前容量两倍的新数组,将当前数组的内容复制到新数组中,并将新数组的引用赋值给栈中当前数组。现在,可用给这个数组添加新值了。
程序清单 10-8 StackOfIntegers.java
public class StackOfIntegers {
private int[] elements;
public int size;
public static final int DEFAULT_CAPACITY = 16;
/**
* Construct a stack with the default capacity 16
*/
public StackOfIntegers(){
this(DEFAULT_CAPACITY);
}
/**
* Construct a stack with the specified maximum capacity
*/
public StackOfIntegers(int capacity){
elements = new int[capacity];
}
/**
* Push a new integer to the top of the stack
*/
public void push(int value){
if (size >= elements.length){
int[] temp = new int[elements.length * 2];
System.arraycopy(elements,0,temp,0,elements.length);
elements = temp;
}
elements[size++] = value;
}
/**
* Return and remove the top element from the stack
*/
public int pop(){
return elements[--size];
}
/**
* Return the top element from the stack
*/
public int peek(){
return elements[size - 1];
}
/**
* Test whether the stack is empty
*/
public boolean empty(){
return size == 0;
}
/**
* Return the number of elements in the stack
*/
public int getSize(){
return size;
}
}
10.7 将基本数据类型值作为对象处理
- 基本数据类型不是对象,但是可以使用Java API 中的包装类来包装成一个对象。
- 出于对性能的考虑,在Java中基本数据类型不作为对象使用。因为处理对象需要额外的系统开销,所以,如果将基本数据类型当作对象,就会给语言性能带来负面影响。然而,Java中的许多方法需要将对象作为参数。Java提供了一个方便的方法,即将基本数据类型合并为或者说包装成对象(例如,将int类包装成Integer类,将double类包装成Double类,将char包装成Character类)。通过使用包装类,可以将基本数据类型值作为对象处理。Java在java.lang包里为基本数据类型提供了Boolean、Character、Double、Float、Byte、Short、Integer和Long等包装类。Boolean类包装了布尔值true或者false。本节使用Integer和Double类为例介绍数值包装类。
- 大多数基本类型的包装类的名称与对应的基本数据类型名称一样,第一个字母要大写。对应int 的Integer 和对应char的Character 例外。
- 数值包装类相互之间都非常相似。每个都各自包含了doubleValue( )、floatValue( )、intValue( )、longValue( )、shortValue( )和byteValue( )等方法。这些方法将对象”转换“为基本类型值。Integer类和Double类的主要特征如图10-14所示。
- 既可以用基本数据类型值也可以用表示数值的字符串来构造包装类。例如,new Double(5.0)、new Double(“5.0”)、new Integer(5) 和 new Integer(“5”)。
- 包装类没有无参构造方法。所有包装类的实例都是不可变的,这意味着一旦创建对象后,它们的内部值就不能在改变。
- 每一个数值包装类都有常量MAX_VALUE和MIN_VALUE。MAX_VALUE表示对应的基本数据类型的最大值。对于Byte、Short、Integer 和 Long 而言,MIN_VALUE 表示对应的基本类型byte、short、int和long的最小值。对Float 和Double类而言,MIN_VALUE表示float 型和double型的最小正值。下面的语句显示最大整数(2 147 483 647)、最小正浮点数(1.4E - 45),以及双精度浮点数的最大值(1.79769313486237570e + 308d):
System.out.println("The maximum integer is " + Integer.MAX_VALUE);
System.out.println("The maximum positive float is " + Float.MIN_VALUE);
System.out.println("The maximum double - precision floating -point numebr is " +
Double.MAX_VALUE);
- 每个数值包装类都包含各自方法doubleValue( )、floatValue( )、intValue( )、longValue( )和shortValue( )。这些方法返回包装对象对应的double、float、int、long或short值。例如:
new Double(12.4).intValue() return 12;
new Integer(12).doubleValue() return 12.0;
- 回顾下String类中包含compareTo方法用于比较两个字符串。数值包装类中包含compareTo方法用于比较两个数值,并且如果该数值大于、等于、小于另外一个数值时,分别返回1、0、1 。例如,
new Double(12.4).compareTo(new Double(12.3)) return 1;
new Double(12.3).compareTo(new Double(12.3)) return 0;
new Double(12.3).compareTo(new Double(12.51)) return -1;
- 数值包装类有一个有用的静态方法valueOf(String s)。该方法创建一个新对象,并将它初始化为指定字符串表示的值。例如,
Double doubleObject = Double valueOf("12.4");
Integer integerObject = Integer.valueOf("12");
我们已经使用过Integer类中的parseInt方法将一个数值字符串转换为一个int值,也使用过Double类中的parseDouble方法将一个数值字符串转变为一个double值。每个数值包装类都有两个重载方法,将数值字符串转换为正确的以10(十进制)或指定值为基数的数值。
//These two methods are in the Byte class
public static byte parseByte(String s);
public static byte parseByte(String s, int radix);
//These two methods are in the Short class
public static short parseShort(String s);
public static short parseShort(String s, int radix);
//These two methods are in the Integer class
public static int parseInt(String s);
public static int parseInt(String s, int radix);
//These two methods are in the Long class
public static long parseLong(String s);
public static long parseLong(String s, int radix);
//These two methods are in the Float class
public static float parseFloat(String s);
public static float parseFloat(String s, int radix);
//These two methods are in the Double class
public static double parseDouble(String s);
public static double parseDouble(String s, int radix);
- 例如:
Integer.parseOmy("11",2) return 3;
Integer.parseOmy("12",8) return 10;
Integer.parseOmy("13",10) return 13;
Integer.parseOmy("1A",16) return 26;
- Integer.parseInt(“12”, 2)会引起一个运行时错误,因为12不是二进制数。
- 注意,可以使用format方法将一个十进制数转换为十六进制数,例如,
String.format("%x", 26) return 1A;
10.8 基本类型和包装类类型之间的自动转换
- 根据上下文环境,基本数据类型值可以使用包装类自动转换成一个对象,反之也可以。
- 将基本类型值转换为包装类对象的过程称为装箱(boxing)。相反的转换过程称为拆箱(unboxing)。Java运行基本类型和包装类类型之间进行自动转换。如果一个基本的类型值出现在需要对象的环境中,编译器会将基本类型值进行自动装箱;如果一个对象出现在需要基本类型值的环境中,编译器会将对象进行自动拆箱。这称为自动装箱和自动拆箱。
- 例如,可以用自动装箱将图a中的语句简化为图b中的语句:
- 由于可以自动拆箱,下面a中的语句等同于b中的语句。
- 考虑下面的例子:
Integer[] intArray = {1,2,3};
System.out.println(intArray[0] + intArray[1] + intArray[2]);
- 在第一行中,基本类型值1、2和3被自动装箱成对象new Integer(1)、new Integer(2)和new Integer(3)。第二行中,对象intArray[0]、intArray[1] 和 intArray[2]被自动拆箱为int值,然后相加。
10.9 BigInteger 和 BigDiecimal 类
- BigInteger 类和 BigDiecimal 类可用于表示任意大小和精度的整数或者十进制数
- 如果要进行非常大的数的计算或者高精度浮点值的计算,可以使用 java.math包中的 BigInteger类 和 BigDiecimal 类。它们都是不可变的。long类型的最大整数值为long.MAX_VALUE(即9 223 372 036 854 775 807),而BigInteger 的实例可以表示任意大小的整数。可以使用new BigInteger(String) 和 new BigDecimal(String) 来创建 BigInteger 和 BigDiecimal 的实例,使用add、subtract、multiple、divide和remainder 方法进行算术运算, 使用compareTo 方法比较两个大数字。例如,下面的代码创建了两个 BigInteger 对象并且将它们进行相乘:
BigInteger a = new BigInteger("9223372036854775807");
BigInteger b = new BigInteger("2");
BigInteger c = a.multiply(b); //9223372036854775807 * 2
System.out.println(c);
- 输出为18446744073709551614
- BigDiecimal 对象可以达到任意精度。如果不能终止运行,那么divide 方法会抛出ArithmeticException 异常。但是,可以使用重载的divide(BigDecimal d, int scale, int roundingMode)方法来指定scale 值和舍入方式来避免这个异常,这里的scale 是指小数点后最小的整数位数。例如,下面的代码创建BigDecimal 对象并做除法,其中scale 值为20,舍入方式为 BigDecimal.ROUND_UP。
BigDecimal a = new BigDecimal(1.0);
BigDecimal b = new BigDecimal(3);
BigDecimal c = a.divide(b,20,BigDecimal.ROUND_UP)
System.out.println(c);
- 输出为0.3333 3333 3333 3333 3334
- 注意,一个整数的阶乘可能会非常大。程序清单10-9给出可以返回任意整数的阶乘的方法。
程序清单 10-9 StackOfIntegers.java
import java.math.BigInteger;
import java.util.Scanner;
public class LargeFactorial {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
System.out.print("Enter an integer: ");
int n = input.nextInt();
System.out.println(n + " ! is \n" + factorial(n));
}
public static BigInteger factorial(long n) {
BigInteger result = BigInteger.ONE;
for (int i = 0; i < n; i++) {
result = result.multiply(new BigInteger(i + ""));
}
return result;
}
}
- BigInteger.ONE 是定义在BigInteger类中的常量。Big Integer.ONE 和 new BigInteger(“1”) 是一样的。
- 通过调用multiply 方法,得到了一个新的结果
10.10 String 类
- String 对象是不可改变的。字符一旦创建,内容不能再改变
- 在4.4节中介绍了字符串。你已经知道字符串是对象,可以调用charAt(index)方法得到字符串中指定位置的字符。length( ) 方法返回字符串的大小,substring 方法返回字符串中的子串, indexOf 和lastIndexOf 方法返回第一个或者最后一个匹配的字符或者字符串,equals 和compareTo 方法比较两个字符串,trim( )方法将字符串两端的空白裁剪掉,toLowerCase( ) 和toUpperCase( )方法分别返回字符串的小写和大写形式,本节中我们将更加深入地讨论字符串。
- String 类中有13 个构造方法以及40多个处理字符串的方法。这不仅在程序设计中非常有用,而且也是一个学习类和对象的很好的例子。
10.10.1 构造字符串
- 可以用字符串字面值或字符数组创建一个字符串对象。使用如下语法,用字符串字面值创建一个字符串:
String newString = new String(stringLitera);
- 参数StringLiteral 是一个括在双引号内的字符序列。下面的语句为字符串字面值”Welcome to Java“ 创建一个String 对象message:
String message = new String ("Welcome to Java");
- Java将字符串字面值看作String 对象。所以,下面的语句是合法的:
String message = "Welcome to Java";
- 还可以使用字符数组创建一个字符串。例如,下述语句构造一个字符串”Good Day“:
char[] charArray = {'G','o','o','d',' ','D','a','y'};
String message = new String (charArray);
- String 变量存储的是对String 对象的引用, String 对象里存储的才是字符串的值。严格地讲, 术语String 变量、String 对象和字符串值是不同的。但在大多数情况下,它们之间的区别是可以忽略的。为简单起见,经常使用术语字符串表示String 变量、String 对象和字符串的值。
10.10.2 不可变字符串与驻留字符串
- String 对象是不可变的,它的内容是不能改变的。下列代码会改变字符串的内容吗?
String s = "Java";
s = "HTML";
- 答案是不能。第一条语句创建了一个内容为
"Java"
的String 对象,并将其引用赋值给s。第二条语句创建了一个内容为”HTML”的新String 对象,并将其引用赋值给s。赋值后第一个String 对象仍然存在,但是不能再访问它了,因为变量s现在指向了新的对象,如图10-15所示。 - 因为字符串在程序设计中是不可变的,但同时又会频繁地使用,所以Java虚拟机为了提高效率并节约内存,对具有相同字符序列的字符串字面值使用同一个实例。这样的实例称为驻留的(interned)字符串。例如,下面的语句:
String s1 = "Welcome to Java";
String s1 = new String ("Welcome to Java");
String s1 = "Welcome to Java";
System.out.println("s1 == s2 is " + (s1 == s2));
System.out.println("s1 == s3 is " + (s1 == s3));
- 程序结构显示:
s1 == s2 is false
s1 == s3 is true
- 在上述语句中,由于s1 和s3 指向相同的驻留字符串 “Welcome to Java”, 因此,s1 == s3 为true。但是s1 == s2 为false,这是因为尽管s1和s2的内容相同,但它们是不同的字符串对象。
10.10.3 替换和拆分字符串
- String 类提供了替换和拆分字符串的方法,如图10-16所示。
- 一旦创建了字符串,它的内容就不能改变。但是,方法replace、replaceFirst 和 replaceAll 会返回一个源自原始字符串的新字符串(并未改变原始字符串!)。方法replace 有好几个版本,它们实现用新的字符或子串替换字符串中的某个字符或子串。
- 例如:
"Welcome".replace('e','A') 返回一个新的字符串,WAlcomA.
"Welcome".replaceFirst('e','AB') 返回一个新的字符串,WABlcome.
"Welcome".replace('e','AB') 返回一个新的字符串,WABlcomAB.
"Welcome".replace('el','AB') 返回一个新的字符串,WABcomA.
- split 方法可以使用指定的分隔符从字符串中提取标记。例如,下面的代码:
String[] tokens = "Java#HTML#Perl".split("#");
for(int i = 0 ; i < tokens.length; i++)
System.out.print(tokens[i] + " ");
- 显示
Java HTML Perl
10.10.4 使用模式匹配、替换和拆分
- 我们经常需要编写代码来验证用户输入,比如检测输入是否是一个数字,或者是否是一个全部小写字母的字符串,或者是否是一个社会安全号。如何编写这类代码呢?一个简单有效地完成该任务的方法是使用正则表达式
- 正则表达式(regular expression)(缩写regex)是一个字符串,用于描述匹配一个字符集的模式。可以通过指定某个模式来匹配、替换或拆分一个字符串。这是一种非常有用且功能强大的特性。
- 从String 类中的matches 方法开始。乍一看,matches方法和equals 方法非常相似。例如,下面两条语句的值均为true:
"Java".matches("Java");
"Java".equals("Java");
- 在前面语句中的”Java.*”是一个正则表达式。它不仅可以匹配固定的字符串,还能匹配一组遵从某种模式的字符串。例如,下面语句的求值结构均为true:
"Java is fun".matches("Java.*")
"Java is cool".matches("Java.*")
"Java is powerful".matches("Java.*")
- 在前面语句中的”Java.“是一个正则表达式。它描述的字符串模式是以字符串Java开始的,后面紧跟任意0个或多个字符。这里,子串.与0个或多个字符相匹配。
- 下面语句的求值结果为true。
"440-02-4534".matches("\\d{3}-\\d{2}-\\d{4}")
- 这里的\d 表示单个数字, \d{3}。
- 方法replaceAll、replaceFirst 和 split 也可以和正则表达式结合在一起使用。例如,下面的语句用字符串NNN替换”a+b$#c”中的$、+ 或者#,然后返回一个新字符串。
String s = "a+b$#c".replaceAll("[$+#]","NNN");
System.out.println(s);
- 这里,正则表达式[$+#] 指定匹配$、+ 或者#的模式。所以,输出是a NNN b NNN NNN c。
- 下面的语句以标点符号作为分隔符,将字符串拆分组成字符串数组。
C# tyokens = Java,C?C#,C++".split("[.,:;?]");
for (inn i = 0; i < tokens.length; i++)
System.out.println(tokens[i]);
- 这里,正则表达式[. , : ; ?] 指定匹配
.
、,
,:
,;
或者?
的模式。这里的每个字符都是拆分字符串的分隔符。因此,这个字符串就被拆分成Java、C、C#和C++,并保存在数组tokens 中。正则表达式对起步阶段的学生来讲可能会比较复杂。基于这个原因,本节只介绍了两个简单的模式。若要进一步学习,请参照补充材料H。
10.10.5 字符串与数组之间的转换
- 字符串不是数组,但是字符串可以转换成数组,反之亦然。为了将字符串转换成一个字符数组,可以使用toCharArray方法。例如,下述语句将字符串”Java”转换成一个数组:
char[] chars = "Java".toCharArray();
- 因此,chars[0]为
'J'
,chars[1]为'a'
。chars[2]为'v'
,chars[3]为'a'
- 还可以使用方法getChars(int sreBegin, int srcEnd, char[ ] dst, int dstBegin) 将下标从srcBegin 到 srcEnd -1的子串复制到字符数组dst 中下标从dstBegin 开始的位置。例如,下面的代码将字符串”CS3720”中下标从2 到6-1 的子串”3720”复制到字符数组dst 中下标从4开始的位置:
char[] dst = {'J','A','V','A','1','3','0','1'};
"CS3720".getChars(2,6,dst,4);
- 这样,dst 就变成了
{'J','A','V','A','3','7','2','0' }
。为了将一个字符数组转换成字符串,应该使用构造方法String(char [])或者方法valueOf(char[])。例如,下面的语句使用String 构造方法由一个数组构造一个字符串:String str = new String(new char[]{'J','a','v','a'});
- 下面的语句使用valueOf方法由一个数组构造一个字符串:
String str = String.valueOf(new char[] {'J','a','v','a'});
10.10.6 将字符和数值转换成字符串
- 回顾以下,我们可以使用Double.parseDouble(str)或者Integer.parseInt(str)将一个字符串转换为一个double值或者一个int值,也可以使用字符串的连接操作符来讲字符或者数组转换为字符串。另外一种将数字转换为字符串的方法是使用重载的静态valueOf方法。该方法可以用于将字符和数值转换成字符串,如图10-17所示。
- 例如,为了将double 值5.44 转换成字符串,可以使用String.valueOf(5.44)。返回值是由字符
'5'
、'.'
、'4'
和'4'
构成的字符串。
10.10.7 格式化字符串
- String 类包含静态方法format ,它可以创建一个格式化的字符串。调用该方法的语法是:
String.format(format,item1,item2, ... ,itemk);
- 这个方法与printf 方法类似,只是format方法返回一个格式化的字符串,而printf方法显示一个格式化的字符串。例如:
String s = String.format("%7.2f%6d%-4s",45.556,14,"AB");
System.out.println("s");
- 显示
□□45.56□□□□14AB□□
- 这里,小方形框表示一个空格。
- 注意,
System.out.printf(format,item1,item2, ... ,itemk);
- 等价于
System.out.print(String.format(format, item1, item2, ... , itemk))
10.11 StringBuilder 类和 StringBuffer 类
- StringBuilder 和 StringBuffer 类似于 String 类,区别在于String 类是不可改变的。
- 一般来说,使用字符串的地方都可以使用StringBuilder/StringBuffer 类。StringBuilder / StringBuffer 类比String 类更灵活。可以给一个StringBuilder 或 StringBuffer 中添加、插入或追加新的内容,但是String 对象一旦创建,它的值就确定了。
- StringBuffer 类中修改缓冲区的方法是同步的,这意味着只有一个任务被允许执行该方法,除此之外,StringBuilder 类与 StringBuffer 类是很相似的。如果是多任务并发访问,就使用StringBuffer ,因为这种情况下需要同步以防止StringBuffer 损坏。并发编程将在第32章介绍。而如果是单任务访问,使用StringBuilder会更有效。StringBuffer 和 StringBuilder 中的构造方法以及其他方法几乎是完全一样的。本节介绍StringBuilder。在本节的所有用到StringBuilder 的地方都可以替换StringBuffer。程序可以不经任何其他修改进行编译和运行。
- StringBuilder 类由3个构造方法和30多个用于管理该构造器以及修改该构建器中字符串的方法。可以使用构造方法创建一个空的构建器或从一个字符串创建一个构建器,如图10-18所示。
10.11.1 修改StringBuilder 中的字符串
- 可以使用如图10-19 中列出的方法,在字符串构建器的末尾追加新内容,在字符串构建器的特定位置插入新的内容,还可以删除或替换字符串构建器中的字符。
- StringBuilder 类提供了几个重载方法,可以将boolean、char、char[]、double、float、int、long和String类型值追加到字符串构建器。例如,下面的代码将字符串和字符追加到stringBuilder,构成新的字符串”Welcome to Java”。
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("Welcome");
stringBuilder.append(' ');
stringBuilder.append("to");
stringBuilder.append(' ');
stringBuilder.append("Java");
- StringBuilder 类也包括几个重载的方法,可以将boolean、char、char[]、double、float、int、long和String 类型值插入字符串构建器。考虑下面的代码:
stringBuilder.insert(11, "HTML and ");
- 假设在调用insert 方法之前、stringBuilder 包含字符串”Welcome to Java”。上面的代码就在stringBuilder的第11个位置(就在 J 之前) 插入 “HTML and”。新的stringBuilder 值为”Welcome to HTML and Java”。
- 也可以使用两个delete 方法将字符从构建器中的字符串中删除,使用reverse 方法倒置字符串,使用replace 方法替换字符串中的字符,或者使用setCharAt 方法在字符串中设置一个新字符。
- 例如,假设在应用下面的每个方法之前,stringBuilder 包含的是 “Welcome to Java”。
stringBuilder.delete(8,11)将构建器变为Welcome Java。
stringBuilder.deleteCharAt(8)将构建器变为Welcome o Java。
stringBuilder.reverse()将构建器变为avaJ ot emocleW。
stringBuilder.replace(11,15,"HTML")将构建器变为Welcome to HTML。
stringBuilder.setCharAt(0,'w')将构建器变为welcome to Java。
- 除了setCharAt方法之外,所有这些进行修改的方法都做两件事:
- 改变字符串构建器的内容
- 返回字符串构建器的引用
- 例如,下面的语句
StringBuilder stringBuilder = stringBuilder.reverse();
- 将构建器中的字符倒置并把构建器的引用复制给stringBuilder1。这样,stringBuilder 和stringBuilder1 都指向同一个StringBuffer 对象。回顾一下,如果对方法的返回值不感兴趣,也可以将带返回值的方法作为语句调用。在这种情况下,Java 就简单地忽略掉返回值。例如,下面的语句
stringBuilder.reverse();
- 它的返回值就被忽略了。返回StringBuilder 的引用可以使得StringBuilder 方法形成调用链,如下所示:
stringBuilder.reverse().delete(8,11).replace(11,15,"HTML");
- 如果一个字符串不需要任何改变,则使用String 而不要使用StringBuilder。String 比StringBuilder 更加高效
10.11.2 toString、capacity、length、setLength 和charAt 方法
- StringBuilder 类提供了许多其他处理字符串构建器和获取它的属性的方法,如图10-20所示。
- capacity( )方法返回字符串构建器当前的容量。容量是指在不增加构建器大小的情况下能够存储的字符数量。
- length( )方法返回字符串构建器中实际存储的字符数量。setLength(newLength) 方法设置字符串构建器的长度。如果参数newLength 小于字符串构建器的当前长度,则字符串构建器会被截短到恰好能包含由参数newLength 给定的字符个数。如果参乎上newLength 大于或等于当前长度,则给字符串构建器追加足够多的空字符(’\u0000’),使其长度length 变成新参数newLength。参数 newLength 必须大于等于0。
- charAt( index ) 方法返回字符串构建器中指定下标index 位置的字符。下标是基于 0 的, 字符串构建器中的第一个字符的下标为0,第二个字符的下标为1,以此类推。参数index 必须大于或等于0,并且小于字符串构建器的长度。
- 字符串的长度总是小于或等于构建器的容量。长度是存储在构建器中的字符串的实际大小,而容量是构建器的当前大小。如果有更多的字符添加到字符串构建器,超出它的容量,则构建器的容量就会自动增加。在计算机内部,字符串构建器是一个字符数组,因此,构建器的容量就是数组的大小。如果超出构建器的容量,就用新的数组替换现有数组。新数组的大小为2×(之前数组的长度 + 1)
- 可以使用 new StringBuilder( initialCapacity) 创建指定初始容量的String - Builder 。通过仔细地选择初始容量能够使程序更高效。如果容量总是大于构建器的实际使用长度,JVM将永远不需要为构建器重新分配内存。另一方面,如果容量过大将会浪费内存空间。可以使用trimToSize( )方法将容量降到实际的大小。
10.11.3 示例学习:判断回文串时忽略既非字母又非数字的字符
- 程序清单5-14 考虑字符串中的所有字符来检测字符串是否是回文串。编写一个新程序,检测一个字符串在忽略即非数字字符时是否是一个回文串。
- 下面是解决这个问题的步骤:
- 通过删除既非字符又非数字的字符过滤这个字符串。要做到这一点,需要创建一个空字符串构建器,将字符串中每一个字母或数字字符添加到字符串构建器中,然后从这个构建器返回所求的字符串。可以使用Character 类中的isLetterOrDigit(ch)方法来检测字符ch 是否是字母或数字。
- 倒置过滤后的字符串得到一个新字符串。使用equals 方法对倒置后的字符串和过滤后的字符串进行比较
- 完整的程序如程序清单10-10所示
程序清单 10-10 StackOfIntegers.java
package Based_On_Article.The_Textbook_Source_Code.Chapter10;
import java.util.Scanner;
public class PalindromeIgnoreNonAlphanumeric {
/**
* Main method
*/
public static void main(String[] args) {
//Create a Scanner
Scanner input = new Scanner(System.in);
//Prompt the user yp enter a string
System.out.print("Enter a string: ");
String s = input.nextLine();
//Display result
System.out.println("Ignoring nonalphanumeric characters, \nis "
+ s + " a palindrome? " + isPalindrome(s));
}
/**
* Return true if a string is a palindrome
*/
public static boolean isPalindrome(String s) {
//Create a new string by eliminating nonalphanumeric chars
String s1 = filter(s);
//Create a new string that is the reversal of s1
String s2 = reverse(s1);
//Check if the reversal is the same as the original string
return s2.equals(s1);
}
/**
* Create a new String by eliminating nonalphanumeric chars
*/
public static String filter(String s) {
//Create a string builder
StringBuilder stringBuilder = new StringBuilder();
//Examine each char in the string to skip alphanumeric char
for (int i = 0; i < s.length(); i++) {
if (Character.isLetterOrDigit(s.charAt(i))) {
stringBuilder.append(s.charAt(i));
}
}
//Return a new filtered string
return stringBuilder.toString();
}
/**
* Create a new string by reversing a specified string
*/
public static String reverse(String s) {
StringBuilder stringBuilder = new StringBuilder(s);
//Invoke reverse in StringBuilder
stringBuilder.reverse();
return stringBuilder.toString();
}
}
- filter(String s )方法逐个地检测字符串s中的每个字符,如果字符是字母或数字字符,就将它复制到字符串构建器。filter 方法返回构建器中的字符串。reverse (String s)方法创建一个新字符串,这个新串是对给定字符串s的倒置。filter 方法和reverse 方法都会返回一个新字符串。原始字符串并没有改变。
- 程序清单5-14中的程序通过比较字符串两端的一对字符来检测一个字符串是否是回文串,程序清单10-10使用StringBuilder 类 中的reverse 方法倒置字符串,然后比较两个字符串是否相等以判断原始字符串是否是回文串。