一、抽象类:把必须完成的行为交给子类
1. 为什么需要抽象类
矩形、圆都是图形,都可以绘制、计算面积。但只说“一个图形”,还无法确定应该怎么画、怎么算。
因此,可以让父类 Shape 保存共同的数据和已确定的代码,把必须由具体图形完成的方法声明为抽象方法。
- 抽象方法:用
abstract修饰,只有方法声明,没有方法体,以分号结束。 - 抽象类:用
abstract修饰的类,不能直接创建对象,但可以作为引用类型。
abstract class Shape { public abstract void draw(); // 没有 {},不是空方法体 } 如果把 draw() 写成普通的空方法,子类忘记重写也可能通过编译。声明为抽象方法后,编译器会要求具体子类提供实现,从而尽早发现遗漏。
2. 一个完整例子
abstract class Shape { protected String name; protected double area; public Shape(String name) { this.name = name; } public abstract void draw(); public abstract void calcArea(); public double getArea() { return area; } } class Rect extends Shape { private double length; private double width; public Rect(double length, double width) { super("矩形"); this.length = length; this.width = width; } @Override public void draw() { System.out.println(name + ":长=" + length + ",宽=" + width); } @Override public void calcArea() { area = length * width; } } public class AbstractDemo { public static void main(String[] args) { Shape shape = new Rect(3, 4); shape.draw(); shape.calcArea(); System.out.println(shape.getArea()); } } 输出:
矩形:长=3.0,宽=4.0 12.0 执行时要分清三件事:
-
new Rect(3, 4)创建的是矩形对象;super("矩形")初始化其中继承自父类的成员。 -
Shape shape是父类引用,指向这个矩形对象;不能实例化抽象类,不代表不能声明抽象类引用。 -
draw()、calcArea()执行矩形类的实现;getArea()直接复用父类实现。本例需要先计算面积,再读取面积。
3. 必须记住的规则
| 规则 | 原因或含义 |
|---|---|
不能直接 new Shape() | 抽象类不能直接实例化 |
| 有抽象方法的类必须是抽象类 | 类中仍有未完成的行为 |
| 抽象类不一定有抽象方法 | 也可以只是禁止直接创建该类对象 |
| 可以有成员变量、普通方法和构造方法 | 抽象类仍然是类;构造方法供子类初始化父类部分 |
| 具体子类必须实现所有尚未实现的抽象方法 | 如果仍有遗漏,子类也必须声明为 abstract |
抽象方法不能是 private、final、static | private 方法不能被子类重写,final 禁止重写,静态方法不参与实例方法的重写 |
例如,子类只实现了 draw(),仍可继续保持抽象:
abstract class Triangle extends Shape { public Triangle() { super("三角形"); } @Override public void draw() { System.out.println("绘制三角形"); } // 尚未实现 calcArea(),所以 Triangle 仍是抽象类 } 二、接口:约定一组共同能力
1. 定义接口与实现接口
接口规定“应该提供哪些行为”。例如,USB 设备都应该能够打开和关闭,电脑只需要按照这套约定使用设备。
用 interface 定义接口,用 implements 让类实现接口:
interface USB { void openDevice(); void closeDevice(); } class Mouse implements USB { @Override public void openDevice() { System.out.println("打开鼠标"); } @Override public void closeDevice() { System.out.println("关闭鼠标"); } public void click() { System.out.println("鼠标点击"); } } class Keyboard implements USB { @Override public void openDevice() { System.out.println("打开键盘"); } @Override public void closeDevice() { System.out.println("关闭键盘"); } public void input() { System.out.println("键盘输入"); } } class Computer { public void useDevice(USB usb) { usb.openDevice(); if (usb instanceof Mouse) { ((Mouse) usb).click(); } else if (usb instanceof Keyboard) { ((Keyboard) usb).input(); } usb.closeDevice(); } } public class USBDemo { public static void main(String[] args) { Computer computer = new Computer(); computer.useDevice(new Mouse()); computer.useDevice(new Keyboard()); } } 输出:
打开鼠标 鼠标点击 关闭鼠标 打开键盘 键盘输入 关闭键盘 useDevice(USB usb) 可以接收不同的实现类对象。调用 openDevice()、closeDevice() 时,会执行实际对象对应的方法,这就是接口参与的多态。
但 USB 没有声明 click(),所以不能直接写 usb.click()。示例先判断实际类型,再向下转型,调用设备特有的方法。新增设备如果只使用打开、关闭功能,不需要修改这两处调用;如果还有特有操作,则需要另行处理。
2. 接口的基本规则
| 规则 | 示例或说明 |
|---|---|
| 接口是引用类型,不能直接实例化 | USB usb = new Mouse(); 正确,new USB() 错误 |
接口中的普通抽象方法隐含 public abstract | void openDevice(); 等价于 public abstract void openDevice(); |
实现接口方法时必须保留 public | 省略后变成包访问权限,缩小了访问范围,编译失败 |
接口字段隐含 public static final | 是常量,不是每个对象各自拥有的实例字段 |
| 接口没有构造方法、实例初始化块和静态初始化块 | 不能把接口当作普通类来初始化 |
| 非抽象实现类必须实现全部尚未实现的抽象方法 | 否则应声明为抽象类 |
接口编译后也生成 .class 文件 | .class 不只用于普通类 |
接口常量示例:
interface DeviceInfo { double VERSION = 3.0; // 等价于 public static final double VERSION = 3.0; } 通过 DeviceInfo.VERSION 访问,不能重新赋值。
接口名采用大驼峰命名。课件中的 IRunning 使用了 I 前缀,但这不是 Java 语法要求;USB、Comparable 也都是接口名。
3. JDK 8 的接口可以有带方法体的方法
不能把“接口方法都是抽象方法”当成不带版本条件的结论。JDK 8 支持 default 方法和 static 方法,它们都有方法体;本章按 JDK 8 规则,不使用接口私有方法。Java SE 8 接口规范
interface Device { void open(); // 抽象方法,由实现类实现 default void close() { System.out.println("关闭设备"); } static void showVersion() { System.out.println("1.0"); } } -
default方法提供默认实现,实现类可以直接使用,也可以重写,通过对象调用。 -
static方法属于接口,通过接口名调用,例如Device.showVersion()。 - 不能只写
void close() { ... }就给接口抽象方法添加方法体;默认方法需要写default。
4. 一个类可以实现多个接口
类继承体现“是什么”,接口常用来描述“具有什么能力”。例如,青蛙是动物,同时具有跑和游泳的能力。
class Animal { protected String name; public Animal(String name) { this.name = name; } } interface IRunning { void run(); } interface ISwimming { void swim(); } class Frog extends Animal implements IRunning, ISwimming { public Frog(String name) { super(name); } @Override public void run() { System.out.println(name + "正在往前跳"); } @Override public void swim() { System.out.println(name + "正在蹬腿游泳"); } } 注意顺序:先写 extends,再写 implements,多个接口用逗号分隔。
如果一个方法只关心“会跑”,就让参数使用 IRunning:
public static void walk(IRunning running) { running.run(); } // 在 main 中调用: // walk(new Frog("小青蛙")); 以后无论传入猫、青蛙还是机器人,只要它实现了 IRunning,walk() 就能使用它,不必要求它们拥有同一个动物父类。
5. 接口之间可以多继承
接口继承接口使用 extends,不是 implements:
interface IAmphibious extends IRunning, ISwimming { } 一个具体类实现 IAmphibious,就需要提供 run() 和 swim() 的实现,相当于遵守两个父接口的要求。
| 关系 | 关键字 | 数量限制 |
|---|---|---|
| 类继承类 | extends | 一个类只能直接继承一个父类 |
| 类实现接口 | implements | 一个类可以实现多个接口 |
| 接口继承接口 | extends | 一个接口可以继承多个接口 |
接口多继承不会让一个类拥有多个直接父类。
6. 抽象类和接口怎么选
| 对比项 | 抽象类 | 接口(JDK 8) |
|---|---|---|
| 主要用途 | 为一组相关子类提供共同状态、实现和抽象行为 | 约定一组能力,供不同类实现 |
| 成员变量 | 可以有实例字段、静态字段 | 字段隐含 public static final |
| 构造方法 | 可以有 | 没有 |
| 方法 | 可以有抽象方法和普通方法 | 可以有抽象方法、default 方法和静态方法 |
| 访问权限 | 按类成员规则设置;抽象方法不能是 private | JDK 8 的接口方法是 public |
| 对类的限制 | 一个类只能直接继承一个父类 | 一个类可以实现多个接口 |
| 能否直接实例化 | 不能 | 不能 |
例如,多个动物要保存各自的 name,适合放在动物父类中;如果还不允许直接创建笼统的“动物”,可以把父类声明为抽象类。不同种类的对象都具有“会跑”的能力,则可以用 IRunning 接口统一描述。
三、比较对象:Comparable 与 Comparator
1. 先确定按什么比较
两个学生对象不能直接使用 s1 > s2 比较。必须明确比较的是分数、姓名,还是其他信息,再通过方法表达比较结果。
compareTo() 和 compare() 都通过返回值的正负号表达先后关系:
| 返回值 | 按当前规则比较的结果 |
|---|---|
小于 0 | 前者排在后者之前 |
等于 0 | 按当前规则比较相等 |
大于 0 | 前者排在后者之后 |
返回值不要求只能是 -1、0、1。例如按分数升序,分数低的排前面;若改为降序,则高分排前面,返回值符号也要相应调整。Comparable 文档
2. Comparable:对象自己提供默认比较规则
让学生类实现 Comparable<Student>,重写 compareTo(),比较“当前对象 this”与“参数对象”。
<Student> 是泛型写法,这里只需理解为:指定参与比较的类型是 Student,这样方法参数可以直接写成 Student。
class Student implements Comparable<Student> { public String name; public int score; public Student(String name, int score) { this.name = name; this.score = score; } @Override public int compareTo(Student other) { if (this.score < other.score) { return -1; } if (this.score > other.score) { return 1; } return 0; } @Override public String toString() { return name + ":" + score; } } 在 main 中使用:
Student s1 = new Student("zhangsan", 10); Student s2 = new Student("lisi", 20); System.out.println(s1.compareTo(s2)); // -1 这里采用分数升序规则:10 < 20,所以返回负数。课件中“高分返回负数”的代码表达的是降序,不能再按升序解释它的结果。
compareTo() 返回 0,只表示按当前规则比较相等。例如两个学生同分,不能据此断定他们是同一个对象或所有属性都相同。
3. Comparator:把不同规则放进独立比较器
如果有时按分数,有时按姓名比较,可以分别创建比较器,而不必反复修改学生类。
下面继续使用上面的 Student,在使用这些类的源文件开头导入 Comparator:
import java.util.Comparator; class ScoreComparator implements Comparator<Student> { @Override public int compare(Student s1, Student s2) { return Integer.compare(s1.score, s2.score); } } class NameComparator implements Comparator<Student> { @Override public int compare(Student s1, Student s2) { return s1.name.compareTo(s2.name); } } -
Integer.compare(a, b)按整数大小返回负数、0或正数,比直接返回a - b更稳妥,避免整数相减溢出。 - 字符串的
compareTo()按字典顺序逐个比较字符,不是比较字符串长度。本例姓名均非null。
在 main 中选择比较器:
Student s1 = new Student("zhangsan", 10); Student s2 = new Student("lisi", 20); Comparator<Student> byScore = new ScoreComparator(); Comparator<Student> byName = new NameComparator(); System.out.println(byScore.compare(s1, s2)); // -1 System.out.println(byName.compare(s1, s2)); // 14,为正数 姓名比较时,首字符 'z' 排在 'l' 后,所以结果为正数。判断结果时关注正负号即可。
| 对比项 | Comparable | Comparator |
|---|---|---|
| 谁实现接口 | 被比较对象所在的类 | 独立的比较器类 |
| 方法 | compareTo(另一个对象) | compare(对象1, 对象2) |
| 调用形式 | s1.compareTo(s2) | 比较器.compare(s1, s2) |
| 适用情况 | 定义类自身的默认顺序 | 提供可切换的比较规则 |
即使 Student 没有实现 Comparable,也可以使用 Comparator<Student> 比较它的对象。
四、对象拷贝:Cloneable、浅拷贝和深拷贝
1. 赋值和克隆不是一回事
Person p2 = p1; 这只是复制引用,p1 和 p2 指向同一个对象,没有生成新对象。
Object.clone() 可以生成对象副本。使用这套机制时,需要注意:
- 类实现
Cloneable。它是标记接口,没有需要实现的方法,用来表明对象允许被Object.clone()克隆。 -
clone()定义在Object中,不是Cloneable声明的方法。 -
Object.clone()是protected,通常在自己的类中重写为public,内部调用super.clone(),方便外部使用。 - 对未实现
Cloneable的对象调用Object.clone(),会抛出CloneNotSupportedException。
2. 浅拷贝:新建外层对象,引用字段仍共享内部对象
class Money { public double amount = 99.99; } class Person implements Cloneable { public int age = 20; public Money money = new Money(); @Override public Person clone() throws CloneNotSupportedException { return (Person) super.clone(); } } public class ShallowCopyDemo { public static void main(String[] args) throws CloneNotSupportedException { Person p1 = new Person(); Person p2 = p1.clone(); System.out.println(p1 == p2); // false System.out.println(p1.money == p2.money); // true p2.age = 30; p2.money.amount = 13.6; System.out.println(p1.age); // 20 System.out.println(p1.money.amount); // 13.6 } } 这里的 throws CloneNotSupportedException 表示把可能出现的克隆异常交给调用方处理。调用方 main 也作相同声明;本例已实现 Cloneable,正常运行不会触发这个异常。异常处理的完整机制暂不展开。
返回类型写为 Person,表示把父类方法的 Object 返回类型缩小为具体子类型,因此调用方不必再强制转换。
浅拷贝后的关系是:
p1 → Person对象A ── money ──┐ ├──→ 同一个Money对象 p2 → Person对象B ── money ──┘ Object.clone() 按字段复制值:
-
age是基本类型,复制数值,所以修改p2.age不影响p1.age。 -
money是引用类型,复制引用,所以两个Person仍指向同一个Money。修改它的amount,通过两个引用都能看到变化。
因此,浅拷贝确实创建了新对象,但没有自动复制它引用的其他对象。
3. 深拷贝:把需要独立的内部对象也复制一份
对于上例,要让两个人的钱互不影响,可以让 Money 也支持克隆,再修改 Person.clone():
class Money implements Cloneable { public double amount = 99.99; @Override public Money clone() throws CloneNotSupportedException { return (Money) super.clone(); } } class Person implements Cloneable { public int age = 20; public Money money = new Money(); @Override public Person clone() throws CloneNotSupportedException { Person copy = (Person) super.clone(); // 先复制外层 Person copy.money = this.money.clone(); // 再复制内部 Money return copy; } } 再次运行前面的测试代码,四次输出依次变为:
false false 20 99.99 对象关系变为:
p1 → Person对象A ── money ──→ Money对象A p2 → Person对象B ── money ──→ Money对象B 本例的 Money 只有基本类型字段,复制到这一层即可。如果内部还有需要独立的可变对象,就还需要继续复制对应对象。
五、内部类:让一个类服务于另一个类
1. 什么是内部类
一个类定义在另一个类的内部时,可以用来描述只为外部类服务的结构,体现封装。
class Outer { class Inner { } } Outer 是外部类,Inner 是内部类。两个类只是写在同一个 .java 文件中、但互不包含,并不构成内部类关系。
本章按常见教学分类区分:静态内部类、实例内部类、局部内部类、匿名内部类。严格说,静态内部类属于“静态嵌套类”。内部类编译后也有独立的 .class 文件。
2. 静态内部类和实例内部类
最重要的区别是:创建对象时是否需要依附一个外部类对象。
class Outer { private int value = 10; private static int count = 20; static class StaticInner { public void show(Outer outer) { System.out.println(count); // 直接访问外部类静态成员 System.out.println(outer.value); // 通过对象访问外部类实例成员 } } class Inner { private int value = 30; public void show() { System.out.println(value); // 内部类自己的 value System.out.println(Outer.this.value); // 所依附外部对象的 value System.out.println(count); // 外部类静态成员 } } } public class InnerDemo { public static void main(String[] args) { Outer outer = new Outer(); Outer.StaticInner s = new Outer.StaticInner(); s.show(outer); Outer.Inner i = outer.new Inner(); i.show(); } } 输出:
20 10 30 10 20 
