第十章 面向对象
10.1 引言
10.2 类的抽象和封装
类的抽象是指将类的实现和类的使用分离开,实现的细节被封装并且对用户隐藏,这被称为类的封装。
Java提供了多层次的抽象。类抽象(class abstraction)是将类的实现和使用分离。类的创建者描述类的功能,让使用者明白如何使用类。从类外可以访问的public构造方法、普通方法和数据域的集合以及对这些成员预期行为的描述,构成了类的合约(class’s contract)。如图10-1所示,类的使用者不需要知道类是如何实现的。实现的细节通过封装对用户隐藏起来,这称为类的封装(class encapsulation)。例如:可以创建一个Circle对象,并且可以在不知道面积是如何计算出来的情况下,求出这个圆的面积。由于这个原因,类也称为抽象数据类型(AbstractData Type, ADT)。
10.3 面向对象思想
面向过程的范式重点在于设计方法。面向对象的范式将数据和方法耦合在一起构成对象。使用面向对象范式的软件设计重点在对象以及对象上的操作。
10.4 类的关系
为了设计类,需要探究类之间的关系。类之间的关系通常有关联、聚合、组合以及继承。
10.4.1 关联
关联是一种常见的二元关系,描述两个类之间的活动。
10.4.2 聚集和组合
聚集是关联的一种特殊形式,代表了两个对象之间的归属关系。聚集对has-a
关系进行建模。所有者对象称为聚集对象,它的类称为聚集类。而从属对象称为被聚集对象,它的类称为被聚集类。
10.5 示例学习:设计Course类
**需求:**假设需要处理课程信息。每门课程都有一个名称以及选课的学生,要能够向/从这个课程添加/删除一个学生。可以使用一个类来对课程建模,如图10-10所示。
**思路:**可以向构造方法Course(String name)
传递一门课程的名称来创建一个Course
对象。可以使用 addstudent(String student)
方法向某门课程添加学生,使用dropStudent(String student)
方法从某门课程中删除一个学生,使用getStudents()
方法可以返回选这门课程的所有学生。假设Course
类是可用的。在一个测试类创建两门课程,并向课程中添加学生。
具体实现:
//Course类
package com.Javabook.Demo;
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) {
// 遍历学生数组,查找要删除的学生
for (int i = 0; i < numberOfStudents; i++) {
if (students[i] != null && students[i].equals(student)) {
// 找到学生,开始删除操作
// 将后面的学生向前移动一位,覆盖要删除的学生
for (int j = i; j < numberOfStudents - 1; j++) {
students[j] = students[j + 1];
}
// 将最后一个元素设置为null,表示数组的结束
students[numberOfStudents - 1] = null;
// 更新学生数量
numberOfStudents--;
return; // 退出方法,因为我们已经找到了并删除了学生
}
}
// 如果没有找到学生,可以选择打印一条消息或者什么都不做
System.out.println("没有发现学生:" + student);
}
}
//TestCourse类
package com.Javabook.Demo;
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("课程1的学生人数:" + course1.getNumberOfStudents());
String[] students = course1.getStudents();
for (int i = 0; i < course1.getNumberOfStudents(); i++) {
System.out.print(students[i] + ",");
}
System.out.println();
System.out.println("课程2的学生人数:" + course2.getNumberOfStudents());
}
}
10.6 示例学习:设计栈类
**需求:**可以定义一个类建模栈。为简单起见,假设该栈存储int数值。因此,命名这个栈类为StackOfIntegers
。这个类的UML图如下所示。
**思路:**栈中的元素都存储在一个名为elements的数组中。创建一个栈的时候,同时也创建了这个数组。类的无参构造方法创建一个默认容量为16的数组。变量 size 记录了栈中元素的个数,而size-1是栈顶元素的下标,如下所示对空栈来说,size为0。
具体实现:
//StackOfIntegers
package com.Javabook.Demo;
public class StackOfIntegers {
private int[] elements;
private int size;
public static final int DEFAULT_CAPACITY = 16;
public StackOfIntegers() {
this(DEFAULT_CAPACITY);
}
public StackOfIntegers(int capacity) {
elements = new int[capacity];
}
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;
}
public int pop() {
return elements[--size];
}
public int peek() {
return elements[size - 1];
}
public boolean empty() {
return size == 0;
}
public int getSize() {
return size;
}
}
//P338_TestStackOfIntegers
package com.Javabook.Demo;
public class P338_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() + " ");
}
}
}
10.7 将基本数据类型值作为对象处理
基本数据类型值不是对象,但是可以使用JavaAPI中的包装类来包装成一个
对象。
出于对性能的考虑,在Java中基本数据类型不作为对象使用。因为处理对象需要额外的系统开销。
然而,Java中的许多方法需要将对象作为参数。Java提供了一个方便的办法,即将基本数据类型合并为或者说包装成对象(例如,将int
包装成Integer
类,将double
包装成Double
类将char
包装成Character
类)。通过使用包装类,可以将基本数据类型值作为对象处理。
大多数基本类型的包装类的名称与对应的基本数据类型名称一样,第一个字母要大写。对应int
的Integer
和对应char
的Character
例外。
10.8 基本类型和包装类类型之间的自动转换
根据上下文环境,基本数据类型值可以使用包装类自动转换成一个对象,反
之也可以。
将基本类型值转换为包装类对象的过程称为装箱(boxing),相反的转换过程称为拆箱(unboxing)。
10.9 BigInteger和BigDecimal类
BigInteger
类和 BigDecimal
类可以用于表示任意大小和精度的整数或者十进制数。
如果要进行非常大的数的计算或者高精度浮点值的计算,可以使用java.math
包中的BigInteger
类和BigDecimal
类。它们都是不可变的。
10.10 String类
String对象是不可改变的。字符串一旦创建,内容不能再改变。
String 变量存储的是对String对象的引用,String对象里存储的才是字符串的值。
10.10.1 不可变字符串与驻留字符串
string 对象是不可变的,它的内容是不能改变的。
下列代码会改变字符串的内容吗?
String s ="Java";
S = "HTML";
答案是不能。
第一条语句创建了一个内容为"Java"
的string
对象,并将其引用赋值给s
。第二条语句创建了一个内容为"HTML"
的新String
对象,并将其引用赋值给s
。赋值后第一个String
对象仍然存在,但是不能再访问它了,因为变量s
现在指向了新的对象,如下所示。
10.10.2 替换和拆分字符串
String类提供了替换和拆分字符串的方法,如下图所示。
10.10.3 使用模式匹配、替换和拆分
正则表达式(regular expression)(缩写regex)是一个字符串,用于描述匹配一个字符串集的模式。可以通过指定某个模式来匹配、替换或拆分一个字符串。这是一种非常有用且功能强大的特性。
10.10.4 字符串与数组之间的转换
字符串不是数组,但是字符串可以转换成数组,反之亦然。为了将字符串转换成一个字符数组,可以使用toCharArray
方法。例如,下述语句将字符串"Java"
转换成一个数组:
char[]chars = "Java".toCharArray();
因此,chars[0]
为J
’,chars[1]
为'a'
,chars[2]
为'v'
,chars[3]
为'a'
。
10.10.5 将字符和数值转换成字符串
我们可以使用 Double.parseDouble(str)
或者Integer.parseInt(str)
将一个字符串转换为一个double
值或者一个int
值,也可以使用字符串的连接操作符来将字符或者数字转换为字符串。
10.10.6 格式化字符串
String
类包含静态方法format
,它可以创建一个格式化的字符串。调用该方法的语法是:
String.format(format,item1,item2.itemk);
这个方法与printf
方法类似,只是format
方法返回一个格式化的字符串。
10.11 StringBuilder类和StringBuffer类
StringBuilder
和StringBuffer
类似于String
类,区别在于String
类是不可改变的。
一般来说,使用字符串的地方都可以使用StringBuilder/StringBuffer
类。StringBuilder/StringBuffer
类比String
类更灵活。
StringBuffer
类中修改缓冲区的方法是同步的,这意味着只有一个任务被允许执行该方法,除此之外,stringBuilder
类与StringBuffer
类是很相似的。
10.11.1 修改StringBuilder中的字符串
可以使用下图中列出的方法,在字符串构建器的末尾追加新内容,在字符串构建器的特定位置插入新的内容,还可以删除或替换字符串构建器中的字符。
10.11.3 示例学习:判断回文串时忽略既非字母又非数字的字符等
**需求:**编写一个新程序检测一个字符串在忽略既非字母又非数字的字符时是否是一个回文串。
思路:
- 通过删除既非字母又非数字的字符过滤这个字符串。要做到这一点,需要创建一个空字符串构建器,将字符串中每一个字母或数字字符添加到字符串构建器中,然后从这个构建器返回所求的字符串。可以使用
Character
类中的isLetter0rDigit(ch)
方法来检测字符ch
是否是字母或数字。 - 倒置过滤后的字符串得到一个新字符串。使用
equals
方法对倒置后的字符串和过滤后的字符串进行比较。
具体实现:
//P355_PalindromeIgnoreNonAlphanumeric
package com.Javabook.Demo;
import java.util.Scanner;
public class P355_PalindromeIgnoreNonAlphanumeric {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.print("请输入一个字符串: ");
String s = sc.nextLine();
System.out.println("忽略非字母数字字符后,这个字符串" + s + "是一个回文吗? " + isPalindrome(s));
}
public static boolean isPalindrome(String s) {
String s1 = filter(s);
String s2 = reverse(s1);
return s2.equals(s1);
}
public static String filter(String s) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
if (Character.isLetterOrDigit(s.charAt(i))) {
sb.append(s.charAt(i));
}
}
return sb.toString();
}
public static String reverse(String s) {
StringBuilder sb = new StringBuilder();
sb.reverse();
return sb.toString();
}
}