一、抽象类:把必须完成的行为交给子类

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 

执行时要分清三件事:

  1. new Rect(3, 4) 创建的是矩形对象;super("矩形") 初始化其中继承自父类的成员。
  2. Shape shape 是父类引用,指向这个矩形对象;不能实例化抽象类,不代表不能声明抽象类引用。
  3. 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 
对比项 静态内部类 实例内部类
声明 static class Inner class Inner
创建对象 new Outer.Inner() outer.new Inner()
是否需要先有外部类对象 不需要 需要
访问外部类静态成员 可以直接访问...Source:https://dev.to/homlander_7c7851e210d2a/13-chou-xiang-lei-he-jie-kou-26lg

Related posts