【Java】面向对象编程(包,继承,组合,多态,抽象类,接口)

1️⃣包(package)

包 (package) 是组织类的一种方式,在系统中查看其实就是文件夹.

使用包的主要目的是保证类的唯一性.

例如, 你在代码中写了一个 Test 类. 然后你的同事也可能写一个 Test 类. 如果出现两个同名的类, 就会冲突, 导致代码不能编译通过.

包的命名规则:

和项目的命名规则相同,全部为小写,多个单词用_分隔,例:(test_project)
变量:小驼峰,例(schoolName)
类名:大驼峰,例(SchoolName)
常量:static final修饰的,全大写,多个单词间用_分隔,例(Integer.MAX_VALUE)

导入包中的类

用 java.util.Date 这种方式引入 java.util 这个包中的 Date 类.

public class Test {
	public static void main(String[] args) {
		java.util.Date date = new java.util.Date();
		// 得到一个毫秒级别的时间戳
		System.out.println(date.getTime());
	}
}

但是这种写法比较麻烦一些, 可以使用 import 语句导入包.

import java.util.Date;//表示导入这个包中的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 都匹配

在这种情况下需要使用完整的类名:
Date date = new Date(); - > java.util.Date date = new java.util.Date();

注意事项:
import 和 C++ 的 #include 差别很大. C++ 必须 #include 来引入其他文件内容, 但是 Java 不需要.import 只是为了写代码的时候更方便. import 更类似于 C++ 的 namespace 和 using.

静态导入

使用 import static 可以导入包中的静态的方法和字段.
这样使用包中的类可不加其包名称,直接通过类名称使用
使用这种方式可以更方便的写一些代码, 例如

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);
    }
}

将类放到包中

基本规则:

  • 在文件的最上方加上一个 package 语句指定该代码在哪个包中.
  • 包名需要尽量指定成唯一的名字, 通常会用公司的域名的颠倒形式(例如 com.bit.demo1 ).
  • 包名要和代码路径相匹配. 例如创建 com.bit.demo1 的包(这里的.相当于一个子路径), 那么会存在一个对应的路径com/bit/demo1 来存储代码.
  • 如果一个类没有 package 语句, 则该类被放到一个默认包中.

包的访问权限控制

如果类中的成员有private,public,protected修饰,就表明了该成员的使用权限。
如果不用上述三者修饰时,此时该成员为默认的包访问权限,即在当前包中可使用。

注意:是仅对当前包中的同级目录下的类可见,不同的包或者子包都不行。

下面的代码给了一个示例. Demo1 和 Demo2 是同一个包中, Test 是其他包中.
Demo1.java

package com.bit.demo;

public class Demo1 {
	int value = 0;
}

Demo2.java

package com.bit.demo;

public class Demo2 {
    public static void Main(String[] args) {
        Demo1 demo = new Demo1();
        System.out.println(demo.value);
    }
}

// 执行结果, 能够访问到 value 变量
10

Test.java

import com.bit.demo.Demo1;

public class Test {
    public static void main(String[] args) {
        Demo1 demo = new Demo1();
        System.out.println(demo.value);
    }
}

// 编译出错
Error:(6, 32) java: value在com.bit.demo.Demo1中不是公共的; 无法从外部程序包中对其进行访问

常见的系统包

  1. java.lang:系统常用基础类(String、Object),此包从JDK1.1后自动导入。
  2. java.lang.reflect:java 反射编程包;
  3. java.net:进行网络编程开发包。
  4. java.sql:进行数据库开发的支持包。
  5. java.util:是java提供的工具程序包(Arrays,Date,jcf(Java集合框架)),非常重要。
  6. java.io:I/O编程开发包。

2️⃣继承

背景

代码中创建的类, 主要是为了抽象现实中的一些事物(包含属性和方法).
有的时候客观事物之间就存在一些关联关系, 那么在表示成类和对象的时候也会存在一定的关联.
如果将每个事物单独用一个类表示出来,由于有一些关联,难免在实现会有许多的代码重复.所以此时需要继承关系,先将共同的属性和方法抽象出来为父类,在子类继承后就有父类的属性和方法,减少了代码重复, 以达到代码重用的效果.

语法规则

class 子类 extends 父类 {

	}
  • 使用 extends 指定父类.
  • Java 中一个子类只能继承一个父类 (而C++/Python等语言支持多继承),但是允许多层继承.
  • 子类会继承父类的所有 public 的字段和方法.
  • 对于父类的 private 的字段和方法, 子类中是无法访问的,只能访问protected和public.
  • 子类的实例中, 也包含着父类的实例. 可以使用 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 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();
    }
}

Cat 和 Bird 继承自 Animal 类, 那么 Cat 和 Bird 在定义的时候就不必再写 name 字段和 eat 方法.
子类继承父类,可在此基础上拓展。例如写的 Bird 类, 就是在 Animal 的基础上扩展出了 fly 方法.

protected 关键字

如果把字段设为 private, 子类不能访问. 但是设成 public, 又违背了我们 “封装” 的初衷.

两全其美的办法就是 protected 关键字.

  • 对于类的调用者来说, protected 修饰的字段和方法是不能访问的
  • 对于类的 子类同一个包的其他类 来说, protected 修饰的字段和方法是可以访问的

小结: Java 中对于字段和方法共有四种访问权限

  • private: 类内部能访问, 类外部不能访问
  • 默认(也叫包访问权限): 类内部能访问, 同一个包中的类可以访问, 其他类不能访问.
  • protected: 类内部能访问, 子类和同一个包中的类可以访问, 其他类不能访问.
  • public : 类内部和类的调用者都能访问

如何进行合适地选择访问权限呢?
我们希望类要尽量做到 “封装”, 即隐藏内部实现细节, 只暴露出 必要 的信息给类的调用者.
因此我们在使用的时候应该尽可能的使用 比较严格 的访问权限. 例如如果一个方法能用 private, 就尽量不要用public.
根据使用场景选择合适的权限.

final 关键字

1.final修饰字段时表示为常量(值和类型都不能修改)。

final int a = 10;
a = 20; // 编译出错

巩固加强:

byte b1 = 1,b2 = 2,b3,b6,b8;
final byte b4 = 4,b5 = 6,b7 = 9;

b3 = b1 + b2;//A
b6 = b4 + b5;//B
b8 = b1 + b4;//C
b7 = b2 + b5;//D

选出上述错误的选项 

结果及分析:

A,C,D
A:两个byte类型的值进行计算时会发生类型提升为int,而b3byte类型,不能接受byte类型的值,所以为错误的。要接收该值的话需要进行强制类型转换为byte
B:两个常量进行想加时,值和类型都不会发生变化,想加过后还是为一个常量值,并且在byte能接收的范围内,所以正确。
C:一个byte和常量进行计算时,还是会提升为int类型。
D:b7为常量值,值不能变化,所以明显错误。

2.final修饰类时,该类不能被继承。例如常见的String类,就被final修饰,不能被继承。

final public class Animal {
	...
}
public class Bird extends Animal {
	...
}
// 编译出错
Error:(3, 27) java: 无法从最终com.bit.Animal进行继承

3.final修饰方法时,表示该方法无法被覆写。

3️⃣组合

和继承类似, 组合也是一种表达类之间关系的方式, 也是能够达到代码重用的效果.

例如表示一个学校:

public class Student {
    ...
}
public class Teacher {
    ...
}
public class School {
    public Student[] students;
    public Teacher[] teachers;
}

组合并没有涉及到特殊的语法(诸如 extends 这样的关键字), 仅仅是将一个类的实例作为另外一个类的字段.
这是设计类的一种常用方式之一.

组合表示 has - a 语义

在刚才的例子中, 我们可以理解成一个学校中 “包含” 若干学生和教师.

继承表示 is - a 语义

在上面的 “动物和猫” 的例子中, 我们可以理解成一只猫也 “是” 一种动物.

4️⃣多态

向上转型

在上面的继承关系中, 可以写形如下面的代码:

Bird bird = new Bird("圆圆");

这个代码也可以写成这个样子

Bird bird = new Bird("圆圆");
Animal bird2 = bird;

// 或者写成下面的方式
Animal bird2 = new Bird("圆圆");

此时 bird2 是一个父类 (Animal) 的引用, 指向一个子类 (Bird) 的实例. 这种写法称为 向上转型.
向上转型这样的写法可以结合 is - a 语义来理解.

向上转型的作用是:统一化参数类型。这样最大的好处就是用一个父类引用就可以接受所有子类实例。

向上转型发生的时机:

  1. 直接赋值
  2. 方法传参,使用的最为广泛
  3. 方法返回

其中直接赋值就如上述代码,下面介绍其他两种:

方法传参

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 (子类) 的实例.

方法返回

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 的实例.

动态绑定

当子类和父类中出现同名方法的时候, 调用会出现什么情况呢?

// 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);
    }
}
// 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 中, 调用某个类的方法, 究竟执行了哪段代码 (是父类方法的代码还是子类方法的代码) , 要看究竟这个引用指向的是父类对象还是子类对象. 这个过程是程序运行时决定的(而不是编译期), 因此称为 动态绑定.

方法重写(Override)

定义

发生在有继承关系的类之间,子类定义了和父类完全相同的方法(方法名相同,参数和返回值相同(至少是向上转型的返回值)),这种情况称为覆写/重写/覆盖

关于重写的注意事项

  1. 重写和重载完全不一样,注意区别,不要混淆。
    方法重载的参数个数、类型、顺序都可以不同,并且与返回值无关,只要求函数名称相同即可。
  1. 普通方法可以重写, static 修饰的静态方法不能重写.
  2. 重写中子类的方法的访问权限不能低于父类的方法访问权限.
  3. 重写的方法返回值类型不一定和父类的方法相同(但是建议最好写成相同, 特殊情况除外).
  4. 在方法重写的时候,不能出现private权限,包访问权限一定要特别小心,子类和父类是否在同一个包。

检查重写

针对重写的方法, 可以使用 @Override 注解来显式指定.

// Bird.java
public class Bird extends Animal {
	@Override
	private void eat(String food) {
		...
	}
}

用在方法上,检查该方法是否正确重写父类的重名方法。

理解多态

多态主要是向上转型 + 方法重写(方法重写是一个可选项)

代码示例: 打印多种形状

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();
    }
}
//运行结果:
♣
○
□

在这个代码中, 分割线上方的代码是 类的实现者 编写的, 分割线下方的代码是 类的调用者 编写的.

当类的调用者在编写 drawMap 这个方法的时候, 参数类型为 Shape (父类), 此时在该方法内部并不知道, 也不关注当前的 shape 引用指向的是哪个类型(哪个子类)的实例. 此时 shape 这个引用调用 draw 方法可能会有多种不同的表现(和 shape 对应的实例相关), 这种行为就称为 多态.

多态顾名思义, 就是 一个引用, 能表现出多种不同形态

使用多态的好处
1.降低使用者的使用门槛

  • 封装是让类的调用者不需要知道类的实现细节.
  • 多态能让类的调用者连这个类的类型是什么都不必知道, 只需要知道这个对象具有某个方法即可.

2.方便扩展
如果要新增一种新的形状, 使用多态的方式代码改动成本也比较低.

class Triangle extends Shape {
@Override
public void draw() {
	System.out.println("△");
	}
}

对于类的调用者来说(drawShapes方法), 只要创建一个新类的实例就可以了, 改动成本很低.

向下转型

要使用向下转型,必须要先发生向上转型——即强制类型转换

// 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) 表示强制类型转换
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. 这时再进行向下转型就比较安全了.

super 关键字

super 表示获取到父类实例的引用. 涉及到两种常见用法.

1.使用了 super 来调用父类的构造器

public Bird(String name) {
	super(name);
}

2.使用 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 关键字, 才是调用父类的方法.

注意super和this的区别

  • super(参数):调用基类中的某一个构造函数(应该为构造函数中的第一条语句)
  • this(参数):调用本类中另一种形成的构造函数(应该为构造函数中的第一条语句)
  • super()和this()类似,区别是,super()从子类中调用父类的构造方法,this()在同一类内调用其它方法。
  • 调用super()必须写在子类构造方法的第一行,否则编译不通过。每个子类构造方法的第一条语句,都是隐含地调用super(),如果父类没有这种形式的构造函数,那么在编译的时候就会报错。
  • 尽管可以用this调用一个构造器,但却不能调用两个。
  • super: 它引用当前对象的直接父类中的成员(用来访问直接父类中被隐藏的父类中成员数据或函数,基类与派生类中有相同成员定义时如:super.变量名 super.成员函数据名(实参)
  • this和super不能同时出现在一个构造函数里面,因为this必然会调用其它的构造函数,其它的构造函数必然也会有super语句的存在,所以在同一个构造函数里面有相同的语句,就失去了语句的意义,编译器也不会通过。
  • this()和super()都指的是对象,所以,均不可以在static环境中使用。包括:static变量,static方法,static语句块。
  • 从本质上讲,this是一个指向本对象的指针, 然而super是一个Java关键字。

在构造方法中调用重写的方法(一个坑)

一段有坑的代码. 我们创建两个类, B 是父类, D 是子类. D 中重写 func 方法. 并且在 B 的构造方法中调用 func。

class B {
    public B() {
// do nothing
        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️⃣抽象类

抽象类是普通类的超集,比普通类多了一些抽象方法。

语法规则

在刚才的打印图形例子中, 我们发现, 父类 Shape 中的 draw 方法好像并没有什么实际工作, 主要的绘制图形都是由Shape 的各种子类的 draw 方法来完成的. 像这种没有实际工作的方法, 我们可以把它设计成一个 抽象方法(abstract method), 包含抽象方法的类我们称为 抽象类(abstract class).

abstract class Shape {
	abstract public void draw();
}
  • 在 draw 方法前加上 abstract 关键字, 表示这是一个抽象方法. 同时抽象方法没有方法体,只有方法声明(没有 { }, 不能执行具体代码).
  • 对于包含抽象方法的类, 必须加上 abstract 关键字表示这是一个抽象类.

注意事项

  1. 抽象类不能直接实例化.
  2. 抽象方法不能是 private 的
  3. 抽象类中可以包含其他的非抽象方法, 也可以包含字段. 这个非抽象方法和普通方法的规则都是一样的, 可以被重写,也可以被子类直接调用
  4. final 修饰类表示不能继承,abstract修饰类表示必须有子类,所以两者不能同时出现。
  5. private修饰方法时表示只在类的内部可见,子类不可见,abstract表示子类必须覆写方法,所以两者不能同时出现。

抽象类的作用

抽象类存在的最大意义就是为了被继承.

抽象类本身不能被实例化, 要想使用, 只能创建该抽象类的子类. 然后让子类重写抽象类中的抽象方法.

并且使用抽象类相当于多了一重编译器的校验.如果不小心误用成父类了,使用普通类编译器是不会报错的. 但是父类是抽象类就会在实例化的时候提示错误, 让我们尽早发现问题,能够及时发现问题在实际开发中是很重要的.

6️⃣接口

接口是抽象类的更进一步. 抽象类中还可以包含非抽象方法 和字段. 而接口中包含的方法都是抽象方法, 字段只能包含静态常量

语法规则

在刚才的打印图形的示例中, 父类 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();
    }
}
  • java中使用interface关键字定义接口,实现接口用implements继承接口。
  • 接口中的方法只有public权限,接口中的方法一定是抽象(abstarct)方法,所以public abstract都可以忽略。
  • 接口中只能包含静态常量( static final),所以static final等关键字都可以省略。

提示:

  1. 创建接口的时候, 接口的命名一般以大写字母I开头.
  2. 接口的命名一般使用 “形容词” 词性的单词.
  3. 阿里编码规范中约定, 接口中的方法和属性不要加任何修饰符号, 保持代码的简洁性.

示例

实现多个接口

Java 中只支持单继承, 一个类只能 extends 一个父类. 但是可以同时实现多个接口, 也能达到多继承类似的效果.

  • 子类在实现多个父接口时,使用,将其分隔。
  • 子类必须要实现父接口中所有抽象方法(子类时普通类)
  • 接口不能直接实例化对象,必须通过子接口来向上转型
  • 子类若同时继承父类和实现接口,先用extends继承父类,再implements实现多个接口。

现在通过类来表示一组动物.

class Animal {
    protected String name;
    public Animal(String name) {
        this.name = name;
    }
}

另外我们再提供一组接口, 分别表示 “会飞的”, “会跑的”, “会游泳的”.

interface IFlying {
    void fly();
}
interface IRunning {
    void run();
}
interface ISwimming {
    void swim();
}

接下来我们创建几个具体的动物

IDEA中使用 ctrl + i 快速实现接口

猫, 是会跑的.

class Cat extends Animal implements IRunning {
    public Cat(String name) {
        super(name);
    }
    @Override
    public void run() {
        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 特性 .

猫是一种动物, 具有会跑的特性.
青蛙也是一种动物, 既能跑, 也能游泳
鸭子也是一种动物, 既能跑, 也能游, 还能飞

接口使用实例

给对象数组排序

给定一个学生类

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 方法, 能否直接使用这个方法呢?

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 方法的方式, 就可以定义比较规则.

我们可以尝试自己实现一个 sort 方法来完成刚才的排序过程(使用冒泡排序)

public static void sort(Comparable[] array) {
    for (int bound = 0; bound < array.length; bound++) {
        for (int cur = array.length - 1; cur > bound; cur--) {
            if (array[cur - 1].compareTo(array[cur]) > 0) {
				// 说明顺序不符合要求, 交换两个变量的位置
                Comparable tmp = array[cur - 1];
                array[cur - 1] = array[cur];
                array[cur] = tmp;
            }
        }
    }
}

再次运行:

sort(students);
System.out.println(Arrays.toString(students));

// 执行结果
[[王五:97], [李四:96], [张三:95], [赵六:92]]

接口间的继承

接口可以继承一个接口, 达到复用的效果. 使用 extends 关键字.

interface IRunning {
	void run();
}
interface ISwimming {
	void swim();
}
// 两栖的动物, 既能跑, 也能游
interface IAmphibious extends IRunning, ISwimming {
}
class Frog implements IAmphibious {
...
}

通过接口继承创建一个新的接口 IAmphibious 表示 “两栖的”. 此时实现接口创建的 Frog 类, 就继续要实现 run 方法,也需要实现 swim 方法.

接口间的继承相当于把多个接口合并在一起.

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
封装是面向对象编程的一项基本原则,它将数据与操作数据的方法打在一起,以实现数据的安全性和可维护性。在Java中,封装主要通过使用访问修饰符来实现。 继承面向对象编程的另一项重要原则,它允许一个类继承另一个类的属性和方法,以减少代码的重复性并增加代码的可复用性。在Java中,继承通过使用关键字“extends”来实现。 多态面向对象编程的又一重要原则,它允许一个对象在不同的环境中表现出不同的行为。在Java中,多态主要通过方法的重写和方法的重载来实现。 对于educoder java面向对象封装继承多态的问题,我们可以通过以下方式回答: 在面向对象编程中,封装是通过使用访问修饰符来保护类的属性和方法,使其不被外部访问和修改。封装可以提高代码的可维护性和安全性。 继承面向对象编程的一项重要特性,它允许一个类继承另一个类的属性和方法,并且可以添加或修改这些属性和方法。通过继承,我们可以减少代码的重复性,增加代码的可复用性。 多态面向对象编程的一项核心原则,它允许一个对象在不同的环境中表现出不同的行为。多态可以通过方法的重写和方法的重载来实现,在Java中,我们可以通过实现接口、使用抽象类和使用重载等方式来实现多态性。 总的来说,封装、继承多态面向对象编程的三项重要概念,它们共同构建了面向对象编程的基础。在Java中,我们可以通过使用访问修饰符来实现封装,通过使用“extends”关键字来实现继承,通过方法重写和重载来实现多态。这些概念和技术能够帮助我们创建更加可维护、可扩展和高效的代码。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

bruin_du

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值