面向对象编程
文章目录
1. 包
包时组织类的一种方式
使用包的主要目的是保证类的唯一性
1.1 导入包中的类
Java中提供了很多现成的类供我们使用,例如:可以使用java.util.Data 这种方式引入java.util这个包中的Data类
public class Test{
public static void main(String [] args){
java.util.Data data = new java.util.Data();
//得到一个毫秒级别的时间戳
System.out.println(data.getTime());
}
}
也可以用import语句导入包
import java.util.Data;
public class Test{
public static void main(String [] args){
java.util.Data data = new java.util.Data();
//得到一个毫秒级别的时间戳
System.out.println(data.getTime());
}
}
如果需要使用java.util中其他的类,可以使用import.java.util.* 但是最好显式的指定要导入的类名,否则还是容易出现冲突情况
import java.util.*;
import java.sql.*;
public class Test{
public static void main(String [] args){
//util 和sql中都存在一个Data这样的类,此时就会出现冲突,编译出错
Date date = new Date();
System.out.println(date.getTime());
}
}
1.2 静态导入
使用import static 可以导入包中的静态的方法和属性
import static java.long.System.*;
public class Test{
public static void main(String [] args){
out.println("hello world");
}
}
使用这种方式可以更方便的写一些代码
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.demo的包,那么就会有一个对应的路径com/demo来存储代码
- 如果一个类没有package语句,则这个类会被放到一个默认包中
操作步骤
- 在IDEA中先建一个包:右键src->new->package
- 在弹出的对话框中输入包名,例如com.demo
- 在包中创建类,右键包名->new->class
1.4 包的访问权限控制
如果包中的某个成员不含public和private关键字,那么这个成员默认是包访问权限,在包内部的其他类可以使用,但是不能在包外部的类中使用。
1.5 常见的系统包
- java.lang:系统常用基础类(String、Object),此包从JDK1.1后自动导入。
- java.lang.reflect:java 反射编程包;
- java.net:进行网络编程开发包。
- java.sql:进行数据库开发的支持包。
- java.util:是java提供的工具程序包。
- java.io:I/O编程开发包。
2. 继承
代码中创建的类, 主要是为了抽象现实中的一些事物(包含属性和方法).
有的时候客观事物之间就存在一些关联关系, 那么在表示成类和对象的时候也会存在一定的关联.
建议每个类都创建一个单独的java文件,类名必须和.java文件名匹配(大小写敏感)
// 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
这时,我们就可以让Cat和Bird分别继承Aniaml类,来达到代码重用的效果
此时, Animal 这样被继承的类, 我们称为 父类 , 基类 或 超类, 对于像 Cat 和 Bird 这样的类, 我们称为 子类, 派生类
2.1 语法规则
基本语法
class 子类 extends 父类{
}
- 使用extends继承父类
- java中一个子类只能继承一个父类
- 继承就是对类于类之间共性的抽取
- 子类会继承父类的所有public的属性和方法
- 对于父类的private的属性和方法,子类无法访问
- 子类的实例中, 也包含着父类的实例. 可以使用 super 关键字得到父类实例的引用
class Animal {
public String name;
public int age;
public Animal(String name,int age) {
this.name = name;
this.age = age;
}
public void eat(String food) {
System.out.println(this.name + "正在吃" +food);
}
}
class Cat extends Animal {
public Cat(String name,int age) {
// 使用 super 调用父类的构造方法.
super(name,age);
}
}
class Bird extends Animal {
public Bird(String name,int age) {
super(name,age); //子类构造的同时,要先帮父类构造,用super()调用父类合适的构造方法,这里是调用父类带有两个参数的构造方法,super()必须写在子类构造方法的第一行
}
public void fly() {
System.out.println(this.name + "正在飞 ");
}
}
public class Test {
public static void main(String[] args) {
Cat cat = new Cat("小黑",19);
cat.eat("猫粮");
Bird bird = new Bird("圆圆",20);
bird.fly();
}
}
//执行结果
小黑正在吃猫粮
圆圆正在飞
extends 英文原意指 “扩展”. 而我们所写的类的继承, 也可以理解成基于父类进行代码上的 “扩展”.
例如我们写的 Bird 类, 就是在 Animal 的基础上扩展出了 fly 方法
当子类和父类出现重名属性时,优先使用子类的属性,要用super.name调用父类的属性
2.2 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
}
}
public,protected,default,private区别(不同包的子类继承的类必须时public,以下才成立)
public:在任何地方都可以访问(同包同类,同包不同类,不同包的子类,不同包的非子类)
protected:只有不同包的非子类不可以访问(同包同类,同包不同类,不同包的子类都可以访问)
不同包的子类中访问时,要在普通方法中使用super关键字访问,
default:默认是包访问权限,同包同类和同包不同类都可以访问,不是同一个包就不能访问
private:只有在同包同类的情况下才能访问,其他情况都不能访问
2.3 final 关键字
修饰一个变量或者字段的时候,表示常量,不能被修改
final修饰一个类,表示被修饰的类就不能被继承
final public class Animal {
...
}
public class Bird extends Animal {
...
}
// 编译出错
Error:(3, 27) java: 无法从最终com.Animal进行继承
平时使用的String字符串类,就是用final修饰的,不能被继承
3. 组合
和继承类似, 组合也是一种表达类之间关系的方式, 也是能够达到代码重用的效果.例如表示一个学校:
public class Student {
}
public class Teacher {
}
public class School {
public Student[] students;
public Teacher[] teachers;
}
组合并没有涉及到特殊的语法(诸如 extends 这样的关键字), 仅仅是将一个类的实例作为另外一个类的字段.这是我们设计类的一种常用方式之一
组合:根据例子,我们可以理解为学校中包含学生和老师
继承:根据例子,我们可以理解为猫也是一种动物
4. 多态
4.1 向上转型
父类引用 引用子类的对象
Bird bird = new Bird("圆圆");
Animal animal = bird;
//或者
Animal animal = new Bird("圆圆");
此时 animal 是父类Animal的引用,引用子类Bird的 对象 ,这种写法称为向上转型 这种直接赋值的方式就发生了向上转型
向上转型发生的时机:
- 直接赋值
- 方法传参
- 方法返回
方法传参
public class Test{
public static void main(String [] args){
Bird bird = new Bird("圆圆");
feed(bird);
}
public static void feed(Animal animal){
animal.eat("谷子");
}
}
//调用feed的传的参数的类型是Bird类型,feed方法设置接收的参数类型是Animal类型,此时就发生了向上转型
方法返回
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 动态绑定
父类引用,引用子类的对象,通过这个父类引用,调用父类和子类同名方法时,会发生动态绑定。
// 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) { //重写了父类中的eat方法
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 方法重写
子类实现父类的同名方法, 并且参数的类型和个数完全相同, 这种情况称为 覆写/重写/覆盖
重写的条件
- 方法名相同
- 参数列表相同
- 返回值相同
- 在父子类的情况下
重写的注意事项
- static方法不能重写,普通方法可以重写
- 重写时,子类的方法的访问权限要大于等于父类的方法的访问权限
- private方法不能重写
- 被final修饰的方法不能重写
- 重写的方法返回值类型相同 除非是协变类型(返回值构成父子类关系)
重载和重写的区别
区别 | 重载 | 重写 |
---|---|---|
概念 | 方法名称相同,参数的类型和个数不同 | 方法名称,返回值类型,参数的类型和个数完全相同 |
范围 | 在同一个类中可以重载 | 在继承关系中 |
限制 | 没有权限要求 | 被重写的方法不饿能有比父类更严格的访问权限控制(子类方法的访问权限要大于等于父类的方法的访问权限控制) |
4.4 静态绑定
根据传入的参数,推导出应该调用的方法,在编译过程中就已经确定了
class Animal {
public String name;
public int age;
public Animal(String name,int age) {
this.name = name;
this.age = age;
}
public void eat(String food) {
System.out.println(this.name + "正在吃" +food);
}
}
class Cat extends Animal {
public Cat(String name,int age) {
super(name,age);
}
public func(int a){
System.out.println("一个参数");
}
public func(int a,int b){
System.out.println("两个参数");
}
public func(int a,int b,int c){
System.out.println("三个参数");
}
}
public class Test{
public static void main(String[] args) {
Cat cat = new Cat("xiaobai",18);
cat.fun(10,20);
//静态绑定,调用两个参数的func方法 也叫编译时多态
//( 编译时多态:利用重载来实现多态,在同一个类中定义多个同名的不同方法来实现多态)
}
}
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 + "正在飞");
}
}
Public class Test{
public static void main(String [] args){
Animal animal = new Bird("圆圆");
animal.eat("谷子");
//我是一只小鸟
//圆圆正在吃谷子
//animal.fly();编译出错 找不到fly方法
}
}
注意事项
编译过程中, animal 的类型是 Animal, 此时编译器只知道这个类中有一个 eat 方法, 没有 fly 方法.
虽然 animal 实际引用的是一个 Bird 对象, 但是编译器是以 animal 的类型来查看有哪些方法的.
通过父类引用只能访问父类自己的成员
对于 Animal animal = new Bird(“圆圆”); 这样的代码
- 编译器检查有哪些方法存在,看的是在Animal这个类型中
- 执行时究竟执行父类的方法还时子类的方法,看的是Bird这个类型
Animal中有eat()这个方法,但是Bird中也有这个方法,并且animal这个引用指向的是Bird这个对象,所以就会执行Bird中的eat()方法,但是Animal中没有fly(),要实现fly()方法,就需要向下转型
Public class Test{
public static void main(String [] args){
Animal animal = new Bird("圆圆");
Bird bird = (Bird)animal;
//(Bird)表示强制类型转换,将animal这个对象的类型又Animal转换为Bird类型
bird.flt();
}
}
//执行结果
圆圆正在飞
但是这样的向下转型有时是不太可靠的,例如
Animal animal = new Cat("xiaomao");
Bird bird = (Bird) animal;
bird.fly();
//执行结果,抛出异常
animal本质上引用的是一个Cat对象,不能转换成Bird对象
所以为了让向下转型更安全,我们可以先判断一下,animal这个对象本质上引用的是不是一个Bird对象
Animal animal = new Cat("小猫");
if(animal instanceof Bird) {
Bird bird = (Bird)animal;
bird.fly();
}
instanceof 可以判定一个引用是否是某个类的实例. 如果是, 则返回 true. 这时再进行向下转型就比较安全了
4.6 super关键字
在子类内部调用父类方法,使用super关键字
- super.属性 调用父类的普通属性
- super.方法 调用父类的普通方法
- super() 调用父类的构造方法
public Bird(String name){
super(name);//调用父类带有一个参数的构造方法
}
使用super调用父类的普通方法
public class Bird extends Animal {
public Bird(String name) {
super(name);
}
@Override
public void eat(String food) {
// 修改代码, 让子调用父类的接口.
super.eat(food);
System.out.println("我是一只小鸟");
System.out.println(this.name + "正在吃" + food);
}
}
在这个代码中, 如果在子类的 eat 方法中直接调用 eat (不加super), 那么此时就认为是调用子类自己的 eat (也就是递归了),而加上 super 关键字, 才是调用父类的方法.
this和super的区别
区别 | this | super |
---|---|---|
概念 | 访问本类中的属性和方法 | 由子类访问父类的属性和方法 |
查找范围 | 先查找本类,如果本类没有就调用父类 | 不查找本类,直接调用父类 |
特殊 | 表示当前对象 | 无 |
在构造方法中调用重写的方法的注意示例:
创建两个类,B是父类,D是子类,D中重写了func方法,并且在B的构造方法中调用func方法
class B {
public B() {
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方法,func方法在D中重写了,会发生动态绑定,调用到D中的func
- 此时,D对象自身还没有构造完成,num处在未初始化的状态,值为0
结论: “用尽量简单的方式使对象进入可工作状态”, 尽量不要在构造方法中调用方法(如果这个方法被子类重写, 就会触发
动态绑定, 但是此时子类对象还没构造完成), 可能会出现一些隐藏的但是又极难发现的问题
4.7 理解多态
有了上面的向上转型, 动态绑定, 方法重写之后, 我们就可以使用 多态(polypeptide) 的形式来设计程序了.
我们可以写一些只关注父类的代码, 就能够同时兼容各种子类的情况
代码示例: 打印多种形状
class Shape{
public void draw(){
System.out.println(" Shape 的draw()");
}
}
class Rect extends Shape{
@Override
public void draw() {
System.out.println("♦");
}
}
class Flower extends Shape{
@Override
public void draw() {
System.out.println("🌼");
}
}
class Triangle extends Shape{
@Override
public void draw() {
System.out.println("🔺");
}
}
//----------------------------------------------
public class Test{
public static void drawMap(Shape shape){
shape.draw();
}
public static void main1(String[] args) {
Rect rect = new Rect();
drawMap(rect);
Flower flower = new Flower();
drawMap(flower);
Triangle triangle = new Triangle();
drawMap(triangle);
}
}
//执行结果
♦
🌼
🔺
在这个代码中, 分割线上方的代码是 类的实现者 编写的, 分割线下方的代码是 类的调用者 编写的.
当类的调用者在编写 drawMap 这个方法的时候, 参数类型为 Shape (父类), 此时在该方法内部并不知道, 也不关注当前的 shape 引用指向的是哪个类型(哪个子类)的实例. 此时 shape 这个引用调用 draw 方法可能会有多种不同的表现(和 shape 对应的实例相关), 这种行为就称为 多态
使用多态的好处时什么?
- 类调用者对类的使用成本进一步降低
- 封装是让类的调用者不需要知道类的实现细节
- 多态能让类的调用者连这个类的类型是什么都不必知道,只需要知道这个对象具有某个方法即可
- 能够降低代码的”圈复杂度“,避免使用大量的if-else
例如我们现在要打印多个形状,如果不用多态,实现代码如下
public static void drawShapes() {
Rect rect = new Rect();
Flower flower = new Flower();
Triangle triangle = new Triangle();
String[] shapes = {"triangle", "rect", "triangle", "rect", "flower"};
for (String shape : shapes) {
if (shape.equals("triangle")) {
triangle.draw();
} else if (shape.equals("rect")) {
rect.draw();
} else if (shape.equals("flower")) {
flower.draw();
}
}
}
如果使用多态,
public static void drawShapes() {
// 我们创建了一个 Shape 对象的数组.
Shape[] shapes = {new Triangle(), new Rect(), new Triangle(),
new Rect(), new Flower()};
for (Shape shape : shapes) {
shape.draw();
}
}
圈复杂度:
圈复杂度是一种描述一段代码复杂程度的方式. 一段代码如果平铺直叙, 那么就比较简单容易理解. 而如果有很多的条件分支或者循环语句, 就认为理解起来更复杂.
因此我们可以简单粗暴的计算一段代码中条件语句和循环语句出现的个数, 这个个数就称为 “圈复杂度”. 如果一个方法的圈复杂度太高, 就需要考虑重构
-
可扩展能力强
如果要新增一种新的形状, 使用多态的方式代码改动成本也比较低.
class Circle extends Shape {
@Override
public void draw() {
System.out.println("⚪");
}
}
对于类的调用者来说(drawShapes方法), 只要创建一个新类的实例就可以了, 改动成本很低.
而对于不用多态的情况, 就要把 drawShapes 中的 if - else 进行一定的修改, 改动成本更高
5. 抽象类
语法规则
在刚才的打印图形例子中, 我们发现, 父类 Shape 中的 draw 方法好像并没有什么实际工作, 主要的绘制图形都是由Shape 的各种子类的 draw 方法来完成的. 像这种没有实际工作的方法, 我们可以把它设计成一个 抽象方法(abstractmethod), **包含抽象方法的类我们称为 抽象类(**abstract class).
abstract class Shape{
abstract public void draw();
}
- 在 draw 方法前加上 abstract 关键字, 表示这是一个抽象方法. 同时抽象方法没有方法体(没有 { }, 不能执行具体代码).
- 对于包含抽象方法的类, 必须加上 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
- 一个普通类,继承了一个抽象类,那么这个普通类中,需要重写这个抽象类中的所有的抽象方法
- 抽象类最大的作用就是被继承
- 一个抽象类A继承了一个抽象类Shape,那么这个抽象类A可以不用实现抽象父类Shape中的抽象方法
- 根据上一点,当A类 再次被一个普通类B继承后,在普通类B中,A和Shape这两个抽象类中的抽象方法必须被重写
abstract class Shape{
abstract public void draw();
}
abstract class A extends Shape{
abstract public void func();
}
class B extends A{
@Override
public void draw() {
}
@Override
public void func() {
}
}
- 抽象类不能被final修饰,抽象方法也不能被final修饰
6. 接口
接口是抽象类的更进一步. 抽象类中还可以包含非抽象方法, 和字段. 而接口中包含的方法都是抽象方法, 字段只能包含静态常量
6.1 语法规则
在刚才的打印图形的示例中, 我们的父类 Shape 并没有包含别的非抽象方法, 也可以设计成一个接口
interface IShape {
void draw();
}
class Circle 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定义一个接口 interface IA { }
- 接口中的普通方法,不能有具体的实现,如果要实现,只能通过关键字default修饰这个方法
- 接口中,可以有static的方法
- 接口中的方法一定是public,可以省略public
- 接口中的抽象方法,默认是public abstract
- 接口不能被关键字new实例化
- Circle 使用 implements 继承接口. 此时表达的含义不再是 “扩展”, 而是 “实现”
- 当一个类实现了一个接口,就必须要重写接口中的抽象方法
- 在调用的时候同样可以创建一个接口的引用, 对应到一个子类的实例.
interface IShape{
public abstract void draw();
}
class Rect implements IShape {
@Override
public void draw() {
System.out.println("♦");
}
}
class Flower implements IShape {
@Override
public void draw() {
System.out.println("🌼");
}
}
class Triangle implements IShape {
@Override
public void draw() {
System.out.println("🔺");
}
}
class Circle implements IShape {
@Override
public void draw() {
System.out.println("⚪");
}
}
public class Test {
public static void main(String[] args) {
IShape [] iShape = {new Circle(),new Flower(),new Triangle()};
for (IShape shape:iShape) {
drawMap(shape);
}
}
public static void drawMap(IShape iShape){
iShape.draw();
}
//执行结果
⚪
🌼
🔺
接口中只能包含抽象方法,对于字段来说,接口中只能包含静态常量
接口当中的成员变量默认是public static final 修饰的
interface IShape {
void draw();
public static final int num = 10;
}
其中public,static,final的关键字都可以省略,省略后的num仍然表示public的静态常量
draw()普通方法 默认是public abstract 也可以省略
- 我们创建接口的时候, 接口的命名一般以大写字母 I 开头.
- 接口的命名一般使用 “形容词” 词性的单词
- 接口当中的成员变量默认是public static final 修饰的
- 当一个类实现一个接口之后,重写这个接口的方法时,必须加上public
interface A{
public static final int a = 10;
public abstract void func();
}
class AClass implements A{
@Override
public void func() {
}
}
一个类可以通过extends继承一个抽象类或者普通类,只能继承一个类,同时也可以使用implements实现多个接口,接口之间用逗号隔开
interface A{
public static final int a = 10;
public abstract void funcA();
}
interface B{
public abstract void funcB();
}
class AClass implements A,B{
@Override
public void funcA() {
}
@Override
public void funcB() {
}
}
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 + "正在蹬腿游泳");
}
}
//还有一种神奇的动物, 水陆空三栖, 叫做 "鸭子
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 class Test{
public static void walk(IRunning running) {
running.run();
}
public static void main(String[] args) {
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 + "正在用轮子跑");
}
}
public static void main(String[] args) {
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),
};
按照我们之前的理解, 数组我们有一个现成的 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;
再次执行程序, 结果就符合预期了.
对Student实现Comparator接口
class AgeComparator implements Comparator<Student> { //年龄比较器
@Override
public int compare(Student o1, Student o2) {
return o1.age - o2.age;
}
}
class ScoreComparator implements Comparator<Student>{ //分数比较器
@Override
public int compare(Student o1, Student o2) {
return (int)(o1.score - o2.score); //将double强制类型转换成int
}
}
class NameComparator implements Comparator<Student>{//姓名比较器
@Override
public int compare(Student o1, Student o2) {
return o1.name.compareTo(o2.name);//name 是String类型,需要调用name的CompareTO
}
}
public class Test{
public static void main(String[] args) {
Student[] student = new Student[3];
student[0] = new Student(12, "baby", 98.9);
student[1] = new Student(6, "abc", 88.9);
student[2] = new Student(18, "zhangsan", 18.9);
System.out.println(Arrays.toString(student));
AgeComparator ageComparator = new AgeComparator();
Arrays.sort(student,ageComparator);//根据年龄比较
System.out.println(Arrays.toString(student));
ScoreComparator scoreComparator = new ScoreComparator();
Arrays.sort(student,scoreComparator);//根据分数比较
System.out.println(Arrays.toString(student));
NameComparator nameComparator = new NameComparator();
Arrays.sort(student,nameComparator);//根据年龄比较
System.out.println(Arrays.toString(student));
}
}
6.4 接口间的继承
一个接口B可以通过extends来拓展另一个接口A的功能,此时,当一个类C通过implements实现这个接口B的时候,需要重写的方法不仅仅是B的抽象方法,还有B从A拓展来的方法
interface A{
void funcA();
}
interface B extends A{
void funcB();
}
class C implements B{
@Override
public void funcA() {
}
@Override
public void funcB() {
}
}