面向对象编程
1. 包
包 (package) 是组织类的一种方式。
使用包的主要目的是保证类的唯一性。
1.1导入包中的类
Java 中已经提供了很多现成的类供我们使用. 例如
public class Test {
public static void main(String[] args) {
java.util.Date date = new java.util.Date();
// 得到一个毫秒级别的时间戳
System.out.println(date.getTime());
}
}
可以使用 java.util.Date 这种方式引入java.util 这个包中的 Date 类。
但是这种写法比较麻烦一些, 可以使用 import 语句导入包.
import java.util.Date;
public class Test {
public static void main(String[] args) {
Date date = new Date();
// 得到一个毫秒级别的时间戳
System.out.println(date.getTime());
}
}
如果需要使用 java.util 中的其他类, 可以使用 import java.util.*。
import java.util.*;
public class Test {
public static void main(String[] args) {
Date date = new Date();
// 得到一个毫秒级别的时间戳
System.out.println(date.getTime());
}
}
但是我们更建议显式的指定要导入的类名. 否则还是容易出现冲突的情况。
import java.util.*;
import java.sql.*;
public class Test {
public static void main(String[] args) {
// util 和 sql 中都存在一个 Date 这样的类, 此时就会出现歧义, 编译出错
Date date = new Date();
System.out.println(date.getTime());
}
}
// 编译出错
Error:(5, 9) java: 对Date的引用不明确
java.sql 中的类 java.sql.Date 和 java.util 中的类 java.util.Date 都匹配
在这种情况下需要使用完整的类名
import java.util.*;
import java.sql.*;
public class Test {
public static void main(String[] args) {
java.util.Date date = new java.util.Date();
System.out.println(date.getTime());
}
注意事项: import 和 C++ 的 #include 差别很大。C++ 必须 #include 来引入其他文件内容, 但是 Java 不需要。
import 只是为了写代码的时候更方便。 import 更类似于 C++ 的 namespace 和 using
1.2静态导入
使用 import static 可以导入包中的静态的方法和字段。
import static java.lang.System.*;
public class Test {
public static void main(String[] args) {
out.println("hello");
}
}
使用这种方式可以更方便的写一些代码, 例如
import static java.lang.Math.*;
public class Test {
public static void main(String[] args) {
double x = 30;
double y = 40;
// 静态导入的方式写起来更方便一些.
// double result = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
double result = sqrt(pow(x, 2) + pow(y, 2));
System.out.println(result);
}
}
1.3将类放到包中
基本规则
- 在文件的最上方加上一个 package 语句指定该代码在哪个包中.
- 包名需要尽量指定成唯一的名字, 通常会用公司的域名的颠倒形式(例如com.bit.demo1 ).
- 包名要和代码路径相匹配. 例如创建 com.bit.demo1 的包, 那么会存在一个对应的路径 com/bit/demo1 来存 储代码。
- 如果一个类没有 package 语句, 则该类被放到一个默认包中.
1.4包的访问权限控制
我们已经了解了类中的 public 和 private. private 中的成员只能被类的内部使用.
如果某个成员不包含 public 和 private 关键字, 此时这个成员可以在包内部的其他类使用, 但是不能在包外部的类使用.
下面的代码给了一个示例. Demo1 和 Demo2 是同一个包中, Test 是其他包中.
Demo1.java
package demo;
public class Demo1 {
int a = 0;
}
Demo2.java
package demo;
public class Demo2 {
public static void Main(String[] args) {
Demo1 demo = new Demo1();
System.out.println(demo.a);
}
}
执行结果: 能够访问到 a 变量
Test.java
import demo.Demo1;
public class Test {
public static void main(String[] args) {
Demo1 demo = new Demo1();
System.out.println(demo.a);
}
}
// 编译出错
Error:(6, 32) java: value在com.bit.demo.Demo1中不是公共的; 无法从外部程序包中对其进行访问
1.5常见的系统包
- java.lang:系统常用基础类(String、Object),此包从JDK1.1后自动导入。
- java.lang.reflect:java 反射编程包;
- java.net:进行网络编程开发包。
- java.sql:进行数据库开发的支持包。
- java.util:是java提供的工具程序包。(集合类等) 非常重要
- java.io:I/O编程开发包。
2.继承
代码中创建的类, 主要是为了抽象现实中的一些事物(包含属性和方法).
有的时候客观事物之间就存在一些关联关系, 那么在表示成类和对象的时候也会存在一定的关联.
例如, 设计一个类表示动物
// Animal.java
public class Animal {
public String name;
public Animal(String name) {
this.name = name;
}
public void eat(String food) {
System.out.println(this.name + "正在吃" + food);
}
}
// Cat.java
class Cat {
public String name;
public Cat(String name) {
this.name = name;
}
public void eat(String food) {
System.out.println(this.name + "正在吃" + food);
}
}
// Bird.java
class Bird {
public String name;
public Bird(String name) {
this.name = name;
}
public void eat(String food) {
System.out.println(this.name + "正在吃" + food);
}
public void fly() {
System.out.println(this.name + "正在飞 ");
}
}
这个代码我们发现其中存在了大量的冗余代码.
仔细分析, 我们发现 Animal 和 Cat
以及 Bird 这几个类中存在一定的关联关系:
- 这三个类都具备一个相同的 eat 方法, 而且行为是完全一样的
- 这三个类都具备一个相同的 name 属性, 而且意义是完全一样的
- 从逻辑上讲, Cat 和 Bird 都是一种 Animal (is - a 语义)
此时我们就可以让 Cat 和 Bird 分别继承 Animal 类, 来达到代码重用的效果。
此时, Animal 这样被继承的类, 我们称为 父类 , 基类 或 超类, 对于像 Cat 和 Bird 这样的类, 我们称为 子类, 派生类。
和现实中的儿子继承父亲的财产类似, 子类也会继承父类的字段和方法, 以达到代码重用的效果。
2.1语法规则
基本语法
class 子类 extends 父类 {
}
- 使用 extends 指定父类。
- Java 中一个子类只能继承一个父类 (而C++/Python等语言支持多继承)。
- 子类会继承父类的所有 public 的字段和方法.
- 对于父类的 private 的字段和方法, 子类中是无法访问的。
- 子类的实例中, 也包含着父类的实例. 可以使用 super 关键字得到父类实例的引用。
对于上面的代码, 可以使用继承进行改进. 此时我们让 Cat 和 Bird 继承自 Animal 类, 那么 Cat 在定义的时候就不必再写 name 字段和 eat 方法。
class Animal {
public String name;
public Animal(String name) {
this.name = name;
}
public void eat(String food) {
System.out.println(this.name + "正在吃" + food);
}
}
class Cat extends Animal {
public Cat(String name) {
// 使用 super 调用父类的构造方法.
super(name);
}
}
class Bird extends Animal {
public Bird(String name) {
super(name);
}
public void fly() {
System.out.println(this.name + "正在飞 ");
}
}
public class Test {
public static void main(String[] args) {
Cat cat = new Cat("小黑");
cat.eat("猫粮");
Bird bird = new Bird("圆圆");
bird.fly();
}
}
运行结果:
extends 英文原意指 “扩展”. 而我们所写的类的继承, 也可以理解成基于父类进行代码上的 “扩展”. 例如我们写的 Bird 类,就是在 Animal 的基础上扩展出了 fly 方法.
如果我们把 name 改成 private, 那么此时子类就不能访问了.
class Bird extends Animal {
public Bird(String name) {
super(name);
}
public void fly() {
System.out.println(this.name + "正在飞 ");
}
}
// 编译出错
Error:(19, 32) java: name 在 Animal 中是 private 访问控制
2.2protected 关键字
刚才我们发现, 如果把字段设为 private, 子类不能访问. 但是设成 public, 又违背了我们 “封装” 的初衷
两全其美的办法就是 protected 关键字
- 对于类的调用者来说, protected 修饰的字段和方法是不能访问的
- 对于类的 子类 和 同一个包的其他类 来说, protected 修饰的字段和方法是可以访问的
// Animal.java
public class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
public void eat(String food) {
System.out.println(this.name + "正在吃" + food);
}
}
// Bird.java
public class Bird extends Animal {
public Bird(String name) {
super(name);
}
public void fly() {
// 对于父类的 protected 字段, 子类可以正确访问
System.out.println(this.name + "正在飞 ");
}
}
// Test.java 和 Animal.java 不在同一个 包 之中了.
public class Test {
public static void main(String[] args) {
Animal animal = new Animal("小动物");
System.out.println(animal.name); // 此时编译出错, 无法访问 name
}
}
小结: Java 中对于字段和方法共有四种访问权限
- private: 类内部能访问, 类外部不能访问
- 默认(也叫包访问权限): 类内部能访问, 同一个包中的类可以访问, 其他类不能访问
- protected: 类内部能访问, 子类和同一个包中的类可以访问, 其他类不能访问
- public : 类内部和类的调用者都能访问
No | 范围 | private | default | protected | public |
---|---|---|---|---|---|
1 | 同一包中的同一类 | √ | √ | √ | √ |
2 | 同一包中的不同类 | √ | √ | √ | |
3 | 不同包中的子类 | √ | √ | ||
4 | 不同包中的非子类 | √ |
我们希望类要尽量做到 “封装”, 即隐藏内部实现细节, 只暴露出 必要 的信息给类的调用者.
2.3更复杂的继承关系
刚才我们的例子中, 只涉及到 Animal, Cat 和 Bird 三种类. 但是如果情况更复杂一些呢?
针对 Cat 这种情况, 我们可能还需要表示更多种类的猫
这个时候使用继承方式来表示, 就会涉及到更复杂的体系
// Animal.java
public Animal {
...
}
// Cat.java
public Cat extends Animal {
...
}
// ChineseGardenCat.java
public ChineseGardenCat extends Cat {
...
}
// OrangeCat.java
public Orange extends ChineseGardenCat {
...
}
......
如刚才这样的继承方式称为多层继承, 即子类还可以进一步的再派生出新的子类.
- 我们写的类是现实事物的抽象. 而我们真正所遇到的项目往往比较复杂, 可能会涉及到一系列复杂的概念,都需要我们使用代码来表示,所以我们真实项目中所写的类也会有很多。类之间的关系也会更加复杂。
- 但是, 一般我们不希望出现超过三层的继承关系. 如果继承层次太多,就需要考虑对代码进行重构了。如果想从语法上进行限制继承, 就可以使用final 关键字
2.4final 关键字
曾经我们学习过 final 关键字, 修饰一个变量或者字段的时候, 表示 常量 (不能修改)。
final int a = 10;
a = 20; // 编译出错
final 关键字也能修饰类, 此时表示被修饰的类就不能被继承。
final public class Animal {
...
}
public class Bird extends Animal {
...
}
// 编译出错
Error:(3, 27) java: 无法从最终Animal进行继承
final 关键字的功能是 限制 类被继承
“限制” 这件事情意味着 “不灵活”。在编程中, 灵活往往不见得是一件好事。灵活可能意味着更容易出错。
是用 final 修饰的类被继承的时候, 就会编译报错, 此时就可以提示我们这样的继承是有悖这个类设计的初衷的。
3.组合
和继承类似, 组合也是一种表达类之间关系的方式, 也是能够达到代码重用的效果。
例如表示一个学校:
public class Student {
...
}
public class Teacher {
...
}
public class School {
public Student[] students;
public Teacher[] teachers;
}
组合并没有涉及到特殊的语法(诸如 extends 这样的关键字), 仅仅是将一个类的实例作为另外一个类的字段。
这是我们设计类的一种常用方式之一.大家要注意体会两种语义的区别。
- 组合表示 has - a 语义
在刚才的例子中, 我们可以理解成一个学校中 “包含” 若干学生和教师。- 继承表示 is - a 语义
在上面的 “动物和猫” 的例子中, 我们可以理解成一只猫也 “是” 一种动物。
4. 多态
4.1向上转型
在刚才的例子中, 我们写了形如下面的代码
Bird bird = new Bird("圆圆");
这个代码也可以写成这个样子
Bird bird = new Bird("圆圆");
Animal bird2 = bird;
// 或者写成下面的方式
Animal bird2 = new Bird("圆圆");
此时 bird2 是一个父类 (Animal) 的引用, 指向一个子类 (Bird) 的实例. 这种写法称为 向上转型。
向上转型发生的时机:
- 直接赋值
- 方法传参
- 方法返回
4.1.1方法传参
public class Test {
public static void main(String[] args) {
Bird bird = new Bird("圆圆");
feed(bird);
}
public static void feed(Animal animal) {
animal.eat("谷子");
}
}
// 执行结果
圆圆正在吃谷子
此时形参 animal 的类型是 Animal (基类), 实际上对应到 Bird (父类) 的实例.
4.1.2方法返回
public class Test {
public static void main(String[] args) {
Animal animal = findMyAnimal();
}
public static Animal findMyAnimal() {
Bird bird = new Bird("圆圆");
return bird;
}
}
此时方法 findMyAnimal 返回的是一个 Animal 类型的引用, 但是实际上对应到 Bird 的实例.
4.2动态绑定
当子类和父类中出现同名方法的时候, 再去调用就会出现动态绑定。
对前面的代码稍加修改, 给 Bird 类也加上同名的 eat 方法, 并且在两个 eat 中分别加上不同的日志
// Animal.java
public class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
//Animal的eat方法:
public void eat(String food) {
System.out.println("我是一只小动物");
System.out.println(this.name + "正在吃" + food);
}
}
// Bird.java
public class Bird extends Animal {
public Bird(String name) {
super(name);
}
//bird的eat方法:
public void eat(String food) {
System.out.println("我是一只小鸟");
System.out.println(this.name + "正在吃" + food);
}
}
// Test.java
public class Test {
public static void main(String[] args) {
Animal animal1 = new Animal("圆圆");
animal1.eat("谷子");
Animal animal2 = new Bird("扁扁");
animal2.eat("谷子");
}
}
// 执行结果
我是一只小动物
圆圆正在吃谷子
我是一只小鸟
扁扁正在吃谷子
此时, 我们发现:
- animal1 和 animal2 虽然都是 Animal 类型的引用, 但是 animal1 指向 Animal 类型的实例,animal2 指向 Bird 类型的实例。
- 针对 animal1 和 animal2 分别调用 eat 方法, 发现 animal1.eat() 实际调用了父类的方法, 而
animal2.eat() 实际调用了子类的方法。
因此, 在 Java 中, 调用某个类的方法, 究竟执行了哪段代码 (是父类方法的代码还是子类方法的代码) , 要看究竟这个引用指向的是父类对象还是子类对象。这个过程是程序运行时决定的(而不是编译期), 因此称为 动态绑定。
4.3方法重写
针对刚才的 eat 方法来说:
子类实现父类的同名方法, 并且参数的类型和个数完全相同, 这种情况称为 覆写/重写/覆盖(Override)。
关于重写的注意事项
- 重写和重载完全不一样. 不要混淆(重载要求同名方法参数1.函数的参数个数不同 2.函数的参数类型不同或者参数类型顺序不同)
- 普通方法可以重写, static 修饰的静态方法不能重写
- 重写中子类的方法的访问权限不能低于父类的方法访问权限.
- 重写的方法返回值类型不一定和父类的方法相同(但是建议最好写成相同, 特殊情况除外)
方法权限示例: 将子类的 eat 改成 private
// Animal.java
public class Animal {
public void eat(String food) {
...
}
}
// Bird.java
public class Bird extends Animal {
// 将子类的 eat 改成 private
private void eat(String food) {
...
}
}
// 编译出错
Error:(8, 10) java: com.bit.Bird中的eat(java.lang.String)无法覆盖Animal中的
eat(java.lang.String)
正在尝试分配更低的访问权限; 以前为public
另外, 针对重写的方法, 可以使用 @Override 注解来显式指定.
// Bird.java
public class Bird extends Animal {
@Override
private void eat(String food) {
...
}
}
有了这个注解能帮我们进行一些合法性校验. 例如不小心将方法名字拼写错了 (比如写成 aet), 那么此时编译器就会发现父类中没有 eat 方法, 就会编译报错, 提示无法构成重写。
小结:重载和重写的区别.
NO | 区别 | 重载(overload) | 重写(override) |
---|---|---|---|
1 | 概念 | 方法名相同,参数的类型及个数不同 | 方法名称,返回值类型,参数的类型及个数完全相同 |
2 | 范围 | 一个类 | 继承关系 |
3 | 限制 | 没有权限要求 | 被覆写的方法不能有比父类更严格的访问控制权限 |
4.4理解多态
有了面的向上转型, 动态绑定, 方法重写之后, 我们就可以使用 多态(polypeptide) 的形式来设计程序了。
我们可以写一些只关注父类的代码, 就能够同时兼容各种子类的情况。
代码示例: 打印多种形状
class Shape {
public void draw() {
// 啥都不用干
}
}
//画圈:
class Cycle extends Shape {
@Override
public void draw() {
System.out.println("○");
}
}
//画方形:
class Rect extends Shape {
@Override
public void draw() {
System.out.println("□");
}
}
//画花:
class Flower extends Shape {
@Override
public void draw() {
System.out.println("♣");
}
}
// Test.java
public class Test {
public static void main(String[] args) {
Shape shape1 = new Flower();
Shape shape2 = new Cycle();
Shape shape3 = new Rect();
drawShape(shape1);
drawShape(shape2);
drawShape(shape3);
}
// 打印单个图形
public static void drawShape(Shape shape) {
shape.draw();
}
}
运行结果:
使用多态的好处是什么?
- 类调用者对类的使用成本进一步降低
- 封装是让类的调用者不需要知道类的实现细节。
- 多态能让类的调用者连这个类的类型是什么都不必知道, 只需要知道这个对象具有某个方法即可。
因此, 多态可以理解成是封装的更进一步, 让类调用者对类的使用成本进一步降低。
- 能够降低代码的 “圈复杂度”, 避免使用大量的 if - else
例如我们现在需要打印的不是一个形状了, 而是多个形状. 如果不基于多态, 实现代码如下:
public static void drawShapes() {
Rect rect = new Rect();
Cycle cycle = new Cycle();
Flower flower = new Flower();
String[] shapes = {"cycle", "rect", "cycle", "rect", "flower"};
for (String shape : shapes) {
if (shape.equals("cycle")) {
cycle.draw();
} else if (shape.equals("rect")) {
rect.draw();
} else if (shape.equals("flower")) {
flower.draw();
}
}
}
如果使用使用多态, 则不必写这么多的 if - else 分支语句, 代码更简单.
public static void drawShapes() {
// 我们创建了一个 Shape 对象的数组.
Shape[] shapes = {new Cycle(), new Rect(), new Cycle(),new Rect(), new Flower()};
for (Shape shape : shapes) {
shape.draw();
}
}
- 可扩展能力更强.
如果要新增一种新的形状, 使用多态的方式代码改动成本也比较低.
class Triangle extends Shape {
@Override
public void draw() {
System.out.println("△");
}
}
对于类的调用者来说(drawShapes方法), 只要创建一个新类的实例就可以了, 改动成本很低。
而对于不用多态的情况, 就要把 drawShapes 中的 if - else 进行一定的修改, 改动成本更高.
4.5向下转型
向上转型是子类对象转成父类对象, 向下转型就是父类对象转成子类对象。
// Animal.java
public class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
public void eat(String food) {
System.out.println("我是一只小动物");
System.out.println(this.name + "正在吃" + food);
}
}
// Bird.java
public class Bird extends Animal {
public Bird(String name) {
super(name);
}
public void eat(String food) {
System.out.println("我是一只小鸟");
System.out.println(this.name + "正在吃" + food);
}
public void fly() {
System.out.println(this.name + "正在飞");
}
}
Animal animal = new Bird("圆圆");
animal.eat("谷子");
// 执行结果
圆圆正在吃谷子
animal.fly();
// 编译出错
找不到 fly 方法
注意事项
编译过程中, animal 的类型是 Animal, 此时编译器只知道这个类中有一个 eat 方法, 没有 fly 方法 虽然 animal实际引用的是一个 Bird 对象, 但是编译器是以 animal 的类型来查看有哪些方法的
对于 Animal animal = new Bird(“圆圆”) 这样的代码,
- 编译器检查有哪些方法存在, 看的是 Animal 这个类型
- 执行时究竟执行父类的方法还是子类的方法, 看的是 Bird 这个类型.
那么想实现刚才的效果, 就需要向下转型.
// (Bird) 表示强制类型转换
//将上面通过Animal animal = new Bird("圆圆");定义的animal(Animal类型)强制转换为Brid类型。
Bird bird = (Bird)animal;
bird.fly();
// 执行结果
圆圆正在飞
但是这样的向下转型有时是不太可靠的. 例如
Animal animal = new Cat("小猫");
Bird bird = (Bird)animal;
bird.fly();
// 执行结果, 抛出异常
Exception in thread "main" java.lang.ClassCastException: Cat cannot be cast to Bird
at Test.main(Test.java:35)
animal 本质上引用的是一个 Cat 对象, 是不能转成 Bird 对象的. 运行时就会抛出异常.
所以, 为了让向下转型更安全, 我们可以先判定一下看看 animal 本质上是不是一个 Bird 实例, 再来转换
Animal animal = new Cat("小猫");
if (animal instanceof Bird) {
Bird bird = (Bird)animal;
bird.fly();
}
instanceof 可以判定一个引用是否是某个类的实例. 如果是, 则返回 true. 这时再进行向下转型就比较安全了.
4.6super 关键字
前面的代码中由于使用了重写机制, 调用到的是子类的方法。如果需要在子类内部调用父类方法怎可以使用super 关键字
super 表示获取到父类实例的引用,涉及到两种常见用法。
- 使用 super 来调用父类的构造器(这个代码前面已经写过了)
public Bird(String name) {
super(name);
}
- 使用 super 来调用父类的普通方法
class Animal {
public String name;
public Animal(String name) {
this.name = name;
}
public void eat(String food) {
System.out.println(this.name + "正在吃" + food);
}
}
class Bird extends Animal {
public Bird(String name) {
super(name);
}
@Override
public void eat(String food) {
// 修改代码, 让子类调用父类的接口.
super.eat(food);
System.out.print("我是一只小鸟,");
System.out.println(this.name + "正在吃" + food);
}
}
public class Test {
public static void main(String[] args) {
Bird bird = new Bird("圆圆");
bird.eat("大米");
}
}
运行结果:
在这个代码中, 如果在子类的 eat 方法中直接调用 eat (不加super), 那么此时就认为是调用子类自己的 eat (也就是递归了)。而加上 super 关键字, 才是调用父类的方法。
注意 super 和 this 功能有些相似, 但是还是要注意其中的区别
No | 区别 | this | super |
---|---|---|---|
1 | 概念 | 访问本类的属性和方法 | 由子类访问父类中的属性,方法 |
2 | 查找范围 | 先查找本类,如果本类没有就调用父类 | 不查找本类而直接调用父类 |
3 | 特殊 | 表示当前对象 | 无 |
这里给大家举一个例子更方便理解:
class B {
public B() {
// do nothing
func();//这里没有使用关键字super,对于d(D d = new D();)来说它会优先查找D中有没有func()方法
}
public void func() {
System.out.println("B.func()");
}
}
class D extends B {
private int num = 1;
@Override
public void func() {
System.out.println("D.func() " + num);
}
}
public class Test {
public static void main(String[] args) {
D d = new D();
}
}
// 执行结果
D.func() 0
- 构造 D 对象的同时, 会调用 B 的构造方法
- B 的构造方法中调用了 func 方法, 此时会触发动态绑定, 会调用到 D 中的func
- 此时 D 对象自身还没有构造, 此时 num 处在未初始化的状态, 值为 0.
结论:
“用尽量简单的方式使对象进入可工作状态”, 尽量不要在构造器中调用方法(如果这个方法被子类重写, 就会触发 动态绑定,但是此时子类对象还没构造完成), 可能会出现一些隐藏的但是又极难发现的问题。
5.抽象类
5.1语法规则
在刚才的打印图形例子中, 我们发现, 父类 Shape 中的 draw 方法好像并没有什么实际工作, 主要的绘制图形都是由Shape 的各种子类的 draw 方法来完成的。像这种没有实际工作的方法, 我们可以把它设计成一个 抽象方法(abstract method), 包含抽象方法的类我们称为 抽象类(abstract class)。
abstract class Shape {
abstract public void draw();
}
在 draw 方法前加上 abstract 关键字, 表示这是一个抽象方法。 同时抽象方法没有方法体(没有 { }, 不能执行具体代码)。
对于包含抽象方法的类, 必须加上 abstract 关键字表示这是一个抽象类。
注意事项
- 抽象类不能直接实例化
Shape shape = new Shape();
// 编译出错
Error:(30, 23) java: Shape是抽象的; 无法实例化
- 抽象方法不能是 private 的
abstract class Shape {
abstract private void draw();
}
// 编译出错
Error:(4, 27) java: 非法的修饰符组合: abstract和private
- 抽象类中可以包含其他的非抽象方法, 也可以包含字段。这个非抽象方法和普通方法的规则都是一样的, 可以被重写,也可以被子类直接调用
abstract class Shape {
abstract public void draw();//抽象方法
void func() {//非抽象方法
System.out.println("func");
}
}
class Rect extends Shape {
...
}
public class Test {
public static void main(String[] args) {
Shape shape = new Rect();
shape.func();
}
}
// 执行结果
func
5.2抽象类的作用
抽象类存在的最大意义就是为了被继承。 抽象类本身不能被实例化, 要想使用, 只能创建该抽象类的子类,然后让子类重写抽象类中的抽象方法。
使用抽象类相当于多了一重编译器的校验。 使用抽象类的场景就如上面的代码, 实际工作不应该由父类完成,
而应由子类完成。那么此时如果不小心误用成父类了,使用普通类编译器是不会报错的。 但是父类是抽象类就会在实例化的时候提示错误,
让我们尽早发现问题。
6.接口
接口是抽象类的更进一步。抽象类中还可以包含非抽象方法, 和字段。而接口中包含的方法都是抽象方法, 字段只能包含静态常量。
6.1语法规则
在刚才的打印图形的示例中, 我们的父类 Shape 并没有包含别的非抽象方法, 也可以设计成一个接口
interface IShape {
void draw();
}
class Cycle implements IShape {
@Override
public void draw() {
System.out.println("○");
}
}
public class Test {
public static void main(String[] args) {
IShape shape = new Rect();
shape.draw();
}
}
- 使用 interface 定义一个接口
- 接口中的方法一定是抽象方法, 因此可以省略 abstract
- 接口中的方法一定是public, 因此可以省public
- Cycle 使用 implements 继承接口。此时表达的含义不再是 “扩展”, 而是"实现"
- 在调用的时候同样可以创建一个接口的引用, 对应到一个子类的实例
- 接口不能单独被实例化
扩展(extends) vs 实现(implements) 扩展指的是当前已经有一定的功能了, 进一步扩充功能。
实现指的是当前啥都没有, 需要从头构造出来。
接口中只能包含抽象方法. 对于字段来说,接口中只能包含静态常量(final static).
interface IShape {
void draw();
public static final int num = 10;
}
其中的 public, static, final 的关键字都可以省略. 省略后的 num 仍然表示 public 的静态常量
提示:
1. 我们创建接口的时候, 接口的命名一般以大写字母 I 开头.
2. 接口的命名一般使用 "形容词" 词性的单词.
3. 阿里编码规范中约定, 接口中的方法和属性不要加任何修饰符号, 保持代码的简洁性.
一个错误的代码
interface IShape {
abstract void draw() ; // 即便不写public,也是public
}
class Rect implements IShape {
void draw() {
System.out.println("□") ; //权限更加严格了,所以无法覆写。
}
6.2实现多个接口
有的时候我们需要让一个类同时继承自多个父类. 这件事情在有些编程语言通过 多继承 的方式来实现的。
然而 Java 中只支持单继承, 一个类只能 extends 一个父类。但是可以同时实现多个接口, 也能达到多继承类似的效果。
现在我们通过类来表示一组动物
class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
}
另外我们再提供一组接口, 分别表示 “会飞的”, “会跑的”, “会游泳的”
interface IFlying {
void fly();
}
interface IRunning {
void run();
}
interface ISwimming {
void swim();
}
接下来我们创建几个具体的动物
猫, 是会跑的
class Cat extends Animal implements IRunning {
public Cat(String name) {
super(name);
}
@Override
public void run() {
System.out.println(this.name + "正在用四条腿跑");
}
}
鱼, 是会游的
class Fish extends Animal implements ISwimming {
public Fish(String name) {
super(name);
}
@Override
public void swim() {
System.out.println(this.name + "正在用尾巴游泳");
}
}
青蛙, 既能跑, 又能游(两栖动物)
class Frog extends Animal implements IRunning, ISwimming {
public Frog(String name) {
super(name);
}
@Override
public void run() {
System.out.println(this.name + "正在往前跳");
}
@Override
public void swim() {
System.out.println(this.name + "正在蹬腿游泳");
}
}
提示, IDEA 中使用 ctrl + i 快速实现接口
还有一种神奇的动物, 水陆空三栖, 叫做 “鸭子”
class Duck extends Animal implements IRunning, ISwimming, IFlying {
public Duck(String name) {
super(name);
}
@Override
public void fly() {
System.out.println(this.name + "正在用翅膀飞");
}
@Override
public void run() {
System.out.println(this.name + "正在用两条腿跑");
}
@Override
public void swim() {
System.out.println(this.name + "正在漂在水上");
}
}
上面的代码展示了 Java 面向对象编程中最常见的用法: 一个类继承一个父类, 同时实现多种接口。
继承表达的含义是 is - a 语义, 而接口表达的含义是 具有 xxx 特性 。
- 猫是一种动物, 具有会跑的特性
- 青蛙也是一种动物, 既能跑, 也能游泳
- 鸭子也是一种动物, 既能跑, 也能游, 还能飞
这样设计有什么好处呢? 有了接口之后, 类的使用者就不必关注具体类型, 而只关注某个类是否具备某种能力。
例如, 现在实现一个方法, 叫 “散步”
public static void walk(IRunning running) {
System.out.println("我带着伙伴去散步");
running.run();
}
在这个 walk 方法内部, 我们并不关注到底是哪种动物, 只要参数是会跑的, 就行
Cat cat = new Cat("小猫");
walk(cat);
Frog frog = new Frog("小青蛙");
walk(frog);
// 执行结果
我带着伙伴去散步
小猫正在用四条腿跑
我带着伙伴去散步
小青蛙正在往前跳
甚至参数可以不是 “动物”, 只要会跑!
class Robot implements IRunning {
private String name;
public Robot(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println(this.name + "正在用轮子跑");
}
}
Robot robot = new Robot("机器人");
walk(robot);
// 执行结果
机器人正在用轮子跑
6.3接口使用实例
刚才的例子比较抽象, 我们再来一个更能实际的例子。
给对象数组排序
给定一个学生类
class Student {
private String name;
private int score;
public Student(String name, int score) {
this.name = name;
this.score = score;
}
@Override
public String toString() {
return "[" + this.name + ":" + this.score + "]";
}
}
再给定一个学生对象数组, 对这个对象数组中的元素进行排序(按分数降序).
Student[] students = new Student[] {
new Student("张三", 95),
new Student("李四", 96),
new Student("王五", 97),
new Student("赵六", 92),
};
Arrays.sort(students);
System.out.println(Arrays.toString(students));
// 运行出错, 抛出异常.
Exception in thread "main" java.lang.ClassCastException: Student cannot be cast to
java.lang.Comparable
数组我们有一个现成的 sort 方法, 但是不能直接使用这个方法呢。两个整数是可以直接比较的, 大小关系明确。而两个学生对象的大小关系需要我们额外指定。
让我们的 Student 类实现 Comparable 接口, 并实现其中的 compareTo 方法
class Student implements Comparable {
private String name;
private int score;
public Student(String name, int score) {
this.name = name;
this.score = score;
}
@Override
public String toString() {
return "[" + this.name + ":" + this.score + "]";
}
@Override
public int compareTo(Object o) {
Student s = (Student)o;
if (this.score > s.score) {
return -1;
} else if (this.score < s.score) {
return 1;
} else {
return 0;
}
}
}
在 sort 方法中会自动调用 compareTo 方法. compareTo 的参数是 Object , 其实传入的就是 Student 类型的对象.
然后比较当前对象和参数对象的大小关系(按分数来算).
- 如果当前对象应排在参数对象之前, 返回小于 0 的数字; 如果当前对象应排在参数对象之后, 返回大于 0 的数字;
- 如果当前对象和参数对象不分先后, 返回 0; 再次执行程序, 结果就符合预期了
// 执行结果
[[王五:97], [李四:96], [张三:95], [赵六:92]]
注意事项: 对于 sort 方法来说, 需要传入的数组的每个对象都是 “可比较” 的, 需要具备compareTo 这样的能力. 通过重写 compareTo 方法的方式, 就可以定义比较规则。
6.4接口间的继承
接口可以继承一个接口, 达到复用的效果. 使用 extends 关键字.
interface IRunning {
void run();
}
interface ISwimming {
void swim();
}
// 两栖的动物, 既能跑, 也能游
interface IAmphibious extends IRunning, ISwimming {
}
class Frog implements IAmphibious {
...
}
通过接口继承创建一个新的接口 IAmphibious 表示 “两栖的”。 此时实现接口创建的 Frog 类, 就继续要实现 run 方法,
也需要实现 swim 方法.
接口间的继承相当于把多个接口合并在一起.
6.5总结
核心区别:
抽象类中可以包含普通方法和普通字段, 这样的普通方法和字段可以被子类直接使用(不必重写)
接口中不能包含普通方法, 子类必须重写所有的抽象方法.
No | 区别 | 抽象类(abstract) | 接口(interface) |
---|---|---|---|
1 | 结构组成 | 普通类+抽象方法 | 抽象方法+全局变量 |
2 | 权限 | 各种权限 | public |
3 | 子类关系 | 使用extends关键字继承抽象类 | 使用implements关键字实现接口 |
4 | 关系 | 一个抽象类可以实现若干个接口 | 接口不能继承抽象类,但是接口可以使用extends关键字继承多个父接口 |
5 | 子类限制 | 一个子类只能继承一个抽象类 | 一个子类可以实现多个接口 |