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

一、包

   1.1 导入包中的类
   1.2 静态导入
   1.3 将类放入包中
   1.4 包的访问权限控制

二、继承

   2.1 背景
   2.2 语法规则
   2.3 protected关键字
   2.4 更复杂的继承关系
   2.5 final关键字

三、多态

   3.1 向上转型
   3.2 动态绑定
   3.3 方法重写
   3.4 理解多态
   3.5 向下转型
   3.6 super关键字

四、抽象类

   4.1 语法规则
   4.2 抽象类的使用

五、接口

   5.1 语法规则
   5.2 实现多个接口
   5.3 接口间的继承
   5.4 三种常见接口
       comparable接口
       Comparator接口
       Clonable接口

一、包

包(package)是组织类的一种方式,包的存在是为了保证类的唯一性。

1.1 导入包中的类

在Java中已经提供了很多现成的类供我们使用,我们可以通过import关键字导入这些类,如:

import java.util.Arrays;
class Test{
	public static void main(String[] args){
		int[] array = {1,2,3,4,5};
		system.out.println(Arrays.toString(array));
	}
}
//结果:
[1,2,3,4,5]

    上述代码中,我们通过import关键字导入了一个Arrays类,并调用了Arrays类中的toString方法将数组转为字符串。前面的java.util指的是这个类所在的包,如果要一次性使用 java.util 下的多个类,可以通过 import java.util.*; 的方式一次性导入java.util中的所有类。

    注意,Java中的import与C语言中的#include差别很大,C语言中必须通过#include来引入其他文件内容,但Java不需要,import只是为了写代码时更方便,在Java中是可以不写import的,直接在使用类时加上这个类所在的包也可以,如:

class Test{
	public static void main(String[] args){
		int[] array = {1,2,3,4,5};
		system.out.println(java.util.Arrays.toString(array));
	}
}

    上述代码中,我们并没有通过import导入Arrays类,而是在使用这个类时加上了这个类所在的包,这种写法虽然是正确的,但我们并不推荐这么做,我们推荐的做法还是通过import导入要使用的类。

1.2 静态导入

我们还可以通过import static来导入包中的静态方法和字段,如:

import static java.lang.System.*;
public class Test{
	public static void main(String[] args){
		out.println("Hello World!");
	}
}

使用这种方式可以更方便地写一些代码,如:

import static java.lang.Math.sqrt;
public class Test {
    public static void main(String[] args) {
        double x = 100;
        double y = 9;
        double result = sqrt(x)+sqrt(y);
        //不使用静态导入的写法如下:
        //double result = Math.sqrt(x)+Math.sqrt(y);
    }
}

    可以看出,当需要频繁使用某个特定类时,静态导入确实可以帮助我们减少一定的代码量,但是静态导入会使得代码的可读性变差,因此静态导入在实际开发过程中一般不常用。

1.3 把类放入包中

    我们一般会在代码最上方加上package关键字来指明这个类所在的包,包名是和代码的路径相匹配的,如我们创建了一个com.package.test的包,那么系统会自动生成一个对应的路径com/package/test,如果没有package语句则会被放在默认包中。

1.4 包的访问权限控制

default:包访问权限,一般不加任何修饰,例如:

class Student{
	int age;
}

age的可访问范围就是age所在的包。

注意:包访问权限一般是默认的,即包访问权限不需要任何修饰符

二、继承

2.1 背景

    代码中创建的类, 主要是为了抽象现实中的一些事物(包含属性和方法).
有的时候客观事物之间就存在一些关联关系, 那么在表示成类和对象的时候也会存在一定的关联。例如,设计一个类表示动物:

class Animal{
    String name;

    public Animal(String name) {
        this.name = name;
    }
    public void eat(){
        System.out.println(this.name+" is eating");
    }
}

设计一个类表示狗:

class Dog{
    String name;
    public Dog(String name) {
        this.name = name;
    }
    public void eat(){
        System.out.println(this.name+" is eating");
    }
    public void run(){
        System.out.println(this.name+" is running");
    }
}

设计一个类表示鸟:

class Bird{
    String name;

    public Bird(String name) {
        this.name = name;
    }
    public void eat(){
        System.out.println(this.name+" is eating");
    }
    public void fly(){
        System.out.println(this.name+" is flying");
    }
}

    可以看出,上述代码中存在冗余代码,Animal,Dog,Bird这三个类都具备相同的name属性,并且具有相同的eat()方法,而且意义完全相同,从逻辑上来讲,Dog和Bird都是属于Animal的,那么,我们就可以让Dog和Bird分别继承Animal类,以达到代码重用的效果。

    像Animal这样被继承的类我们称为父类,又叫基类或超类,而Dog,Bird这样的类我们叫做子类或派生类。这里的继承与现实中孩子继承父亲的资产类似,子类也可以继承父类的属性和方法,以达到代码重用的效果。

2.2 语法规则

继承的基本语法:

class 子类 extends 父类{
	...
}
//如:
class Dog extends Animal{
	...
}

    Java中通过extends来实现继承,每个子类只能继承一个父类,子类可以继承父类中的public属性和方法,可以继承protected属性和方法,如果父类和子类在一个包中,则父类的默认访问修饰符属性和方法也是可以被继承的,但子类不会继承父类的private属性和方法,同时无法继承父类的构造方法。

经过继承后的代码如下:

class Animal{
    String name;
    public Animal(String name) {
        this.name = name;
    }
    public void eat(){
        System.out.println(this.name+" is eating");
    }
}
class Dog extends Animal{
    public Dog(String name) {
        super(name);
    }
    public void run(){
        System.out.println(this.name+" is running");
    }
}
class Bird extends Animal{
    public Bird(String name) {
        super(name);
    }
    public void fly(){
        System.out.println(this.name+" is flying");
    }
}

    子类Dog和Bird继承了父类Animal中的name属性和eat()方法,但并不是所有动物都具备run()和fly()的能力,因此我们不能将这两个方法强加到Animal类里,而是在特定子类中写出它们独有的方法和字段。

2.3 protected关键字

    我们已经知道,如果将字段属性设为private,那么子类就无法访问这个字段,而在实际开发中,我们也不应该将字段都设置为public,因为这违背了我们封装的初衷,所以Java中引入了protected关键字。protected关键字修饰的字段可访问的范围是同一个包中的类或是这个变量所在类的子类,这样既可以让子类访问到,又能实现类的封装,让类的调用者无法访问。

在Java中,共有四种访问权限:
private: 类内部能访问, 类外部不能访问
default(也叫包访问权限): 类内部能访问, 同一个包中的类可以访问, 其他类不能访问.
protected: 类内部能访问, 子类和同一个包中的类可以访问, 其他类不能访问.
public : 类内部和类的调用者都能访问

这四种访问权限,在什么情况下使用哪一种呢?

    我们希望类要尽量做到 “封装”, 即隐藏内部实现细节, 只暴露出必要的信息给类的调用者.因此我们在使用的时候应该尽可能的使用 比较严格 的访问权限. 例如如果一个方法能用 private, 就尽量不要用public.
    另外, 还有一种 简单粗暴 的做法: 将所有的字段设为 private, 将所有的方法设为 public. 不过这种方式属于是对访问权限的滥用。

2.4 更复杂的继承关系

    在实际生活中,我们可能会遇到类的多层继承问题,例如狗属于动物,但狗又可以细分为藏獒,柴犬,牧羊犬等,这些狗又可以继续往下细分,结果就会出现一层又一层的继承,即子类继承父类,子类又会有子类,子类又会有子类,然而,我们一般并不希望类与类之间的继承太过复杂,一般类的继承不宜超过三层,如果继承太多,我们就要考虑对代码进行重构了。那么如何限制子类的继承呢?

要从语法上限制子类的继承,我们就要用到final关键字。

2.5 final关键字

我们知道,当final关键字修饰一个变量时,它会将这个变量变为常量,即变量的值不可以再被修改,例如:

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

当final关键字用来修饰类时,它表示这个类不可以再被继承:

final class Animal{
	...
}
class Dog extends Animal{
	...
}
//编译出错

    final 关键字的功能是 限制 类被继承。
    “限制” 这件事情意味着 “不灵活”,在编程中, 灵活往往不见得是一件好事,灵活可能意味着更容易出错。
    使用 final 修饰的类被继承的时候, 就会编译出错, 此时就可以提示我们这样的继承是有悖这个类的设计初衷的。

三、多态

3.1 向上转型

已知Animal为父类,Dog和Bird为子类

我们先实例化一个Dog对象,然后让Animal类型的引用来引用这个对象:

Dog dog = new Dog("旺财");
Animal animal = dog;

这样,父类的引用animal和子类的引用dog就共同引用了我们实例化的对象。

也可以写成如下形式:

Animal animal = new Dog("旺财");

也就是直接创建一个父类的引用,来引用子类的对象,这种写法我们称为向上转型

    上述代码中,我们是直接创建了一个子类的对象然后赋值给了父类的引用,这种方式属于向上转型中的直接赋值方式,另外还有两种向上转型的方式,分别为方法传参和方法返回。

下来介绍方法传参:

public static void main(String[] args) {
	Dog dog = new Dog("旺财");
	func(dog);
}
public static void func(Animal animal){

}

    方法传参就是将实参的子类对象传给形参的父类引用,上述代码中我们将dog引用的子类对象传递给func()方法,func()方法中的父类引用animal接收了这个子类引用,这就是方法传参。

最后是方法返回:

public static void main(String[] args) {
	Animal animal = func();
}
public static Dog func(){
	Dog dog = new dog("旺财");
	return dog;
}

    方法返回就是用父类引用来引用子类的返回值,上述代码中,func()方法的返回值是Dog类型,但我们用Animal类型的引用进行接收,这就是方法返回。

    其实这三种向上转型的方式并没有明显的区别,本质都是用父类的引用来引用子类的对象。

3.2 动态绑定

    我们之前提到了,子类会继承父类的方法,那么,如果子类中出现了和父类同名的方法,当我们再去调用时,又会发生什么情况呢?

我们修改上面的代码,为Dog和Bird里都加入eat()方法,并修改里面的内容:

class Animal{
    String name;
    public Animal(String name) {
        this.name = name;
    }
    public void eat(){
        System.out.println(this.name+" Animal类下的eat()方法");
    }
}
class Dog extends Animal{
    public Dog(String name) {
        super(name);
    }
    public void eat(){
        System.out.println(this.name+" Dog类下的eat()方法");
    }
    public void run(){
        System.out.println(this.name+" is running");
    }
}
class Bird extends Animal{
    public Bird(String name) {
        super(name);
    }
    public void eat(){
        System.out.println(this.name+" Bird类下的eat()方法");
    }
    public void fly(){
        System.out.println(this.name+" is flying");
    }
}
public class Test {
    public static void main(String[] args) {
        Animal animal1 = new Animal("妮妮");
        animal1.eat();
        Animal animal2 = new Dog("旺财");
        animal2.eat();
        Animal animal3 = new Bird("啾啾");
        animal3.eat();
    }
}
//结果:
妮妮 Animal类下的eat()方法
旺财 Dog类下的eat()方法
啾啾 Bird类下的eat()方法

    我们发现,animal1,animal2,animal3虽然都是Animal类型的引用,但当他们引用不同的实例时,调用的方法不同。
    animal1引用了父类的实例,它调用的是父类的eat()方法
    animal2引用了Dog类的实例,它调用的是Dog类的eat()方法
    animal3引用了Bird类的实例,它调用的是Bird类的eat()方法

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

    注意,static,final,private修饰的方法都不可以发生动态绑定,并且发生动态绑定的类之间的关系必须满足子类的访问范围必须不小于父类的访问范围,例如子类由protected修饰,父类由public修饰,那么这两个类之间不可以发生动态绑定。

动态绑定发生的前提:

  1. 父类引用引用了子类的对象
  2. 通过父类引用调用了父类和子类同名的覆盖方法

    这两句话可能有点晦涩难懂,通俗地讲,就是必须发生向上转型和方法的重写。向上转型我们已经从上面了解了,那么什么是方法的重写呢?

3.3 方法重写

    针对上面的eat()方法来说,子类实现了与父类同名的方法,并且参数的个数和类型完全相同,那么这种情况我们称为方法的重写。重写又叫覆写,覆盖。
重写方法的要求:

  1. 方法名相同
  2. 参数列表相同(即参数的个数和类型相同)
  3. 返回值相同
  4. 只能发生在父子类之间

关于重写的注意事项:
static方法不可以被重写,并且子类的访问权限不能低于父类的访问权限。

注意重写和重载的区别:

  1. 重载要求方法名相同,参数的个数和类型不同
    重写要求方法名,参数个数和类型都必须相同
  2. 重载发生在一个类里
    重写发生在具有继承关系的多个类里
  3. 重载不具有访问权限要求
    重写要求子类不能有低于父类的访问范围

3.4 理解多态

    多态是继封装,继承之后的第三个面向对象的特性,多态指的是一个实体具有多种形式,而Java作为一门面向对象的语言,自然可以描述一个事物的多种形态。

多态发生的前提:

  1. 必须发生在子类和父类之间
  2. 父类和子类之间存在方法的重写
  3. 父类引用引用了子类对象

代码示例:

//打印多个形状
class Shape{	//定义父类
    public void draw(){

    }
}
class Circle extends Shape{	//Circle子类继承Shape类
    public void draw(){
        System.out.println("●");
    }
}
class Rect extends Shape{	//Rect子类继承Shape类
    public void draw(){
        System.out.println("♦");
    }
}
class Flower extends Shape{  //Flower子类继承Shape类
    public void draw(){
        System.out.println("❀");
    }
}
/*----------------分割线----------------------*/
public class Test {
    public static void print(Shape shape){
        shape.draw();
    }
    public static void main(String[] args) {
        print(new Flower());
        print(new Circle());
        print(new Rect());
    }
}
//结果:
❀
●
♦

    上述代码中分割线上方是由类的实现者编写的,分割线下方是由类的调用者编写的,当类的调用者在调用print()这个方法时,print()方法的参数类型为Shape,此时类的实现者不关心,也并不知道print()方法中的shape引用指向哪个子类的实例,这个shape引用在调用draw方法时可能会调用Flower类的draw方法,也可能调用Circle类或者Rect类的draw方法,这就是所谓的一个实体的多种表现形式,这种行为就称为多态

3.5 向下转型

    我们已知向上转型是父类引用来引用子类对象,那么向下转型就是子类引用来引用父类对象。相较于向上转型,向下转型并不常见,但也有其用途。

依然是那个动物的例子:

class Animal{	//定义一个Animal父类
    String name;
    public Animal(String name) {
        this.name = name;
    }
    public void eat(){
        System.out.println(this.name+" is eating");
    }
}
class Dog extends Animal{	//定义子类Dog继承Animal
    public Dog(String name) {
        super(name);
    }
    public void run(){
        System.out.println(this.name+" is running");
    }
}
class Bird extends Animal{	//定义子类Bird继承Animal
    public Bird(String name) {
        super(name);
    }
    public void fly(){
        System.out.println(this.name+" is flying");
    }
}

先写一个向上转型:

Animal animal = new Bird("啾啾");
animal.eat();
//结果:
啾啾 is eating

然后执行如下代码:

animal.fly();
//编译错误
java: 找不到符号
  符号:   方法 fly()

在编译过程中,animal的类型是Animal,Animal中并没有fly()方法,虽然animal引用的是一个Bird类型的对象,但编译器是通过animal的类型来查看方法的。

要想让上述代码编译通过,就必须采用向下转型:

Bird bird = (Bird) animal;
bird.fly();
//结果:
啾啾 is flying

但这样的向下转型并不总是可靠的,如:

Animal animal = new Dog("汪汪");
Bird bird = (Bird)animal;
//编译错误,抛出异常:
Dog cannot be cast to Bird

    animal引用的是一个Dog类型的对象,无法转换为Bird类型,运行时便会抛出异常。因此,在向下转型之前,我们要先判断animal是否引用的是Bird实例,然后再进行转型。

3.6 super关键字

    本文中对于super关键字只是粗略地了解了一下,深入了解super关键字请移步我的另一篇博客:
谈谈Java中super关键字的用法和细节,欢迎大佬指正

    在Java中,我们如何在子类中调用父类的方法呢?这时候就要用到super关键字。

使用super来在子类中调用父类的构造方法:

class Animal{
    String name;
    public Animal(String name) {
        this.name = name;
        System.out.println("Animal类的构造方法");
    }
}
class Bird extends Animal{
    public Bird(String name) {
        super(name);	//super调用了父类Animal的构造方法
        System.out.println("Bird类的构造方法");
    }
}
public class Test {
    public static void main(String[] args) {
        Bird bird = new Bird("啾啾");
    }
}
//结果:
Animal类的构造方法
Bird类的构造方法

    在上述代码中,看上去我们只调用了Bird类的构造方法,但在Bird类的构造方法中,我们又通过super关键字,调用了父类的构造方法,需要注意的是,super()中的参数列表必须和父类构造方法中的参数列表相同。

四、抽象类

4.1 语法规则

先举一个上面打印图形的例子:

class Shape{	//创建父类Shape
    public void draw(){

    }
}
class Circle extends Shape{	//创建子类Circle继承Shape
    public void draw(){
        System.out.println("●");
    }
}
class Rect extends Shape{	//创建子类Rect继承Shape
    public void draw(){
        System.out.println("♦");
    }
}
class Flower extends Shape{	//创建子类Flower继承Shape
    public void draw(){
        System.out.println("❀");
    }
}
/*----------------分割线----------------------*/
public class Test {
    public static void print(Shape shape){
        shape.draw();
    }
    public static void main(String[] args) {
        print(new Flower());
        print(new Circle());
        print(new Rect());
    }
}
//结果:
❀
●
♦

    在这段代码中,所有的子类重写了父类中的draw()方法,但父类中的draw()方法似乎没有什么实际工作,所有打印图形的工作都是由子类来完成的,这种没有实际工作的方法,我们可以对他进行处理,将他改为抽象方法,包含抽象方法的类我们叫做抽象类。

那么,如何对这个方法进行处理,使得他变成一个抽样方法呢?

    在父类中draw()方法的public修饰符后加上一个关键字abstract,这个方法就成为了一个抽样方法,抽样方法中存在关键字abstract,那这个抽样方法所在的抽样类也要加上abstract关键字,抽样类的关键字要加在class之前,但是,此时编译器还会报错,是因为这个方法中还存在着方法体,方法体就是方法后面{ }中的部分,那么去掉{ },这个抽样类就变成了下面的样子:

abstract class Shape{
    public abstract void draw();
}

现在,Shape类就被抽象成了一个抽象类,抽象类不可以被实例化,但仍然可以发生向上转型,如:

Shape shape = new Shape();
//编译错误	java: Shape是抽象的; 无法实例化
Shape shape = new Circle();
//编译通过

    注意,抽象类中并不只是包含抽象方法,抽象类中也可以包含成员方法和字段,这些方法和普通方法的使用规则一致,都可以被重写,可以被子类直接调用。

上面提到了抽象类中的普通方法可以被子类重写,那么抽象方法可以被子类重写吗?

  1. 对于抽象子类来说,子类可以不重写父类中的抽象方法。
  2. 对于普通子类(也就是非抽象子类)来说,子类必须重写抽象父类中的所有抽象方法。
  3. 结合上面两条,存在一个普通类C,两个抽象类A,B,C exends B ,B extends A,A中存在抽象方法,但B不需要重写A中的抽象方法,而B中也有抽象方法,那么C就需要同时重写A和B中的抽象方法。

4.2 抽象类的使用

    抽象类存在的最大意义就是被继承,因为抽象类本身不能被实例化,想要使用抽象类,就必须创建抽象类的子类,然后让子类重写抽象类的抽象方法。

既然抽象类本身的意义只是被继承,那我们为什么不直接继承普通类,而是要继承抽象类呢?

    事实上,使用抽象类的场景就如上面的代码, 实际工作不应该由父类完成, 而应由子类完成. 那么此时如果不小心误用成父类了,使用普通类编译器是不会报错的. 但是父类是抽象类就会在实例化的时候提示错误, 让我们尽早发现问题.

五、接口

5.1 语法规则

接口是一种特殊的抽象类。

我们使用interface来定义接口,我们将刚才的Shape抽象类改为IShape接口:

interface Shape{
	public abstract void draw();
}

抽象类中可以包含非抽象方法和字段,但接口中只能包含常量和抽象方法。

我们试着在IShape接口中定义非抽象方法和字段:

interface Shape{
    int num;
     void drawmap(){
         
     }
    public abstract void draw();
}
//编译错误

将drawmap()方法更改为抽象方法,将num用final修饰并为其赋初值:

interface Shape{
    final int num = 0;
     void drawmap();
    public abstract void draw();
}
//编译通过

接口中的方法默认是public abstract类型,因此方法前的public abstract是可以省略的,即:void draw();

类通过implement来继承接口,这里implement的含义为“实现”,即类实现了接口。

通过Circle类来实现IShape接口:

interface Shape{
    void draw();
}
abstract class Circle implements Shape {
    public void draw(){
        System.out.println("●");
    }
}
//结果:

    请注意,draw()方法在接口中是默认public abstract类型,但在Circle类中并不是,因此Circle类中的public不可以省略。

    既然接口是特殊的抽象类,那么和抽象类一样,自然也不能实例化,同理,一个类实现了接口,那么这个类就要重写接口中的所有抽象方法。但抽象类中可以存在构造方法,而接口中不能存在构造方法。

5.2 实现多个接口

    Java中不存在Python那样的多继承方式,Java中的继承只能是单继承,但我们可以通过同时实现多个接口的方式来达到多继承的效果。

    虽然子类只能继承一个父类,但一个类却能实现多个接口,我们通过一个例子来实现多个接口。

首先定义父类和接口:

abstract class Animal{	//定义一个Animal父类
    String name;
    public Animal(String name) {
        this.name = name;
    }
    abstract void eat();
}
interface IRun{		//定义一个IRun接口
    void run();
}
interface ISwim{	//定义一个ISwim接口
    void swim();
}
interface IJump{	//定义一个IJump接口
    void jump();
}
interface IFly{		//定义一个IFly接口
    void fly();
}

我们先定义一个Duck类:

class Duck extends Animal implements IRun,ISwim {
//Duck类继承了Animal类,实现了IRun和ISwim接口
    public Duck(String name) {
        super(name);
    }
        @Override
        public void swim () {
            System.out.println(this.name + " is swimming");
        }
        @Override
        public void eat () {
            System.out.println(this.name + " is eating");
        }
        @Override
        public void run () {
            System.out.println(this.name + " is running");
        }
}

    Duck类继承了父类Animal的eat()方法,又实现了IRun和ISwim接口的run()方法和swim()方法。

再定义一个Bird类:

class Bird extends Animal implements IJump,IFly{
//Bird类继承了Animal方法,实现了IJump和IFly接口
    public Bird(String name) {
        super(name);
    }
    @Override
    void eat() {
        System.out.println(this.name+" is eating");
    }
    @Override
    public void jump() {
        System.out.println(this.name+" is jumping");
    }
    @Override
    public void fly() {
        System.out.println(this.name+" is flying");
    }
}

    Bird类继承了父类Animal的eat()方法,又实现了IJump和IFly接口的jump()方法和fly()方法。

    上面的Duck类和Bird类展示了Java面向对象编程中最常用的方法:一个类继承一个父类,实现多个接口。

    鸭子继承了所有动物共有的特点—吃饭,并且具有鸭子独有的特点—奔跑和游泳,鸟类也继承了所有动物共有的特点—吃饭,鸟类也有它们独有的特点—跳跃和飞,这样写的好处是什么呢?

    好处就是,有了接口之后, 类的使用者就不必关注具体类型, 而只关注某个类是否具备某种能力。

例如,我需要实现一个方法walk(),理论上,会使用run()方法的动物一定会使用walk()方法,那么,在walk()方法内部,我们不需要知道这个动物是什么,只需要关注它是否会run()方法即可,实现代码如下:

public class Test {
	//实现walk()方法
    public static void walk(IRun irun,String name){
    /*因为Duck类实现了IRun接口,所以可以用IRun类型的引用
    来引用Duck类型的对象,与向上转型同理*/
        System.out.println(name+" is walking");
    }
    public static void main(String[] args) {
        Duck duck = new Duck("嘎嘎");
        walk(duck,duck.name);
    }
}
//结果:
嘎嘎 is walking

5.3 接口间的继承

接口可以通过extends关键字来实现继承,以达到复用的效果。

例如,我们可以创建一个 IAmphibious接口来继承ISwim接口和IRun接口,IAmohibious接口代表两栖类动物:

interface ISwim{
    void Swim();
}
interface IFly{
    void fly();
}
interface IAmphibious extends ISwim,IRun{
	//定义一个两栖动物接口,它继承了ISwim接口和IRun接口
}

创建一个Frog类代表青蛙,青蛙实现了两栖类动物这个接口:

class Frog implements IAmphibious{
	//Forg继承了IAmphibious接口,需要重写ISwim和IRun接口的抽象方法
    @Override
    public void run() {
		...
    }
    @Override
    public void swim() {
		...
    }
}

    IAmphibious接口继承ISwim和IRun接口时可以不重写这两个接口中的抽象方法,但定义Frog类时就必须重写run()方法和swim()方法,这点与抽象类的继承同理。

5.4 三种常见接口

comparable接口

当我们要对一个数组进行排序时,可能会用到Arrays类,例如:

int[] array = {1,3,5,2,4};
Arrays.sort(array);	//sort()方法用于对数组按从小到大的顺序进行排序
System.out.println(Arrays.toString(array));//将数组转为字符串后打印
//结果:
[1,2,3,4,5]

我们定义一个Student类,将数组类型由int[ ]改为Student[ ]再试试:

class Student{		//创建一个Student类
    String name;
    int score;
    public Student(String name, int score) {
        this.name = name;
        this.score = score;
    }
}
public class Test {
    public static void main(String[] args) {
            //实例化一个Student类型的数组
        Student[] students = new Student[]{
                new Student("张三",13),
                new Student("李四",60),
                new Student("王五",35),
                new Student("赵六",44)
        };
        Arrays.sort(students);
        System.out.println(Arrays.toString(students));
    }
}
//运行出错,抛出异常
Student cannot be cast to java.lang.Comparable

    仔细思考,我们不难发现,对于int数组,sort()方法是通过数组中每个元素的大小来比较的,但在Student数组中,每个下标都实例化了一个对象,也就是说数组中每个元素都存放的是它们引用的对象的地址,但Java中是不允许地址进行比较的,所以运行会出错。

如果非要进行比较,那就让Student类实现一个comparable接口,并实现接口中的compareTo()方法:

class Student implements Comparable <Student> {	
//Student类实现了comparable接口
    String name;
    int score;

    public Student(String name, int score) {
        this.name = name;
        this.score = score;
    }
    @Override
    public String toString() {	//重写toString方法
        return "Student{" +
                "name='" + name + '\'' +
                ", score=" + score +
                '}';
    }
    @Override
    public int compareTo(Student o) {
    //重写compareTo方法,让数组中的元素按照成绩由低到高排列
        if(this.score<o.score){
            return -1;
        }
        else if(this.score==o.score){
            return 0;
        }
        else {
            return 1;
        }
    }
}
public class Test1 {
    public static void main(String[] args) {
        Student[] students = new Student[]{
                new Student("张三",13),
                new Student("李四",60),
                new Student("王五",35),
                new Student("赵六",44)
        };
        Arrays.sort(students);
        System.out.println(Arrays.toString(students));
    }
}
//结果:
[Student{name='张三', score=13}, Student{name='王五', score=35}, 
Student{name='赵六', score=44}, Student{name='李四', score=60}]

    在上述代码中,在 sort 方法中会自动调用 compareTo 方法. compareTo 的参数是 Object , 其实传入的就是 Student 类型的对象.然后比较当前对象和参数对象的大小关系(按分数来算):

    如果当前对象应排在参数对象之前, 返回小于 0 的数字;
    如果当前对象应排在参数对象之后, 返回大于 0 的数字;
    如果当前对象和参数对象不分先后, 返回 0;

    虽然comparable接口可以让数组元素按照自己的意愿进行排序,但这种写法对类的侵入性较强,如果要更改排序方式,就需要在Student类中修改compareTo()方法,这并不是我们想看到的,因此,我们引入了下一种接口:

comparator接口

先创建一个学生类:

class Student{
    String name;
    int score;
    int age;
    public Student(String name, int score,int age) {
        this.name = name;
        this.score = score;
        this.age = age;
    }
    //重写toString方法
    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", score=" + score +
                ", age=" + age +
                '}';
    }
}

假设我们需要将学生按照成绩从小到大的顺序进行排列,执行如下代码:

/*创建一个ScoreCompare类用于比较学生成绩,实现接口comparator,并重写
接口中的compare方法,使其返回两学生的成绩之差,注意返回值为int类型*/
class ScoreCompare implements Comparator<Student>{
    @Override
    public int compare(Student o1, Student o2) {
        return o1.score-o2.score;
    }
}

类的调用者调用该类中的compare()方法并打印:

public class Test {
    public static void main(String[] args) {
        Student[] students = new Student[]{
                new Student("张三",13,12),
                new Student("李四",60,9),
                new Student("王五",35,16),
                new Student("赵六",44,7)
        };
        ScoreCompare scoreCompare = new ScoreCompare();
        Arrays.sort(students,scoreCompare);
        System.out.println(Arrays.toString(students));
    }
}
//结果:
[Student{name='张三', score=13}, Student{name='王五', score=35}, 
Student{name='赵六', score=44}, Student{name='李四', score=60}]

    如果要更改排序方式,那么只需要添加一个类来重写compare()方法,然后类的调用者直接调用新类中的compare()即可。

假设我们需要将学生按照年龄大小排序,先添加一个新类AgeCompare用于对数组中学生年龄进行排序,并重写compare方法:

class AgeCompare implements Comparator<Student>{
    @Override
    public int compare(Student o1, Student o2) {
        return o1.age-o2.age;
    }
}

类的调用者只需要调用AgeCompare类中的compare()方法:

public class Test {
    public static void main(String[] args) {
        Student[] students = new Student[]{
                new Student("张三",13,12),
                new Student("李四",60,9),
                new Student("王五",35,16),
                new Student("赵六",44,7)
        };
        AgeCompare ageCompare = new AgeCompare();
        Arrays.sort(students,ageCompare);
        System.out.println(Arrays.toString(students));
    }
}
//结果:
[Student{name='赵六', score=44, age=7}, Student{name='李四', score=60, age=9},
 Student{name='张三', score=13, age=12}, Student{name='王五', score=35, age=16}]

可以看到,结果确实是按照年龄由小到大的顺序进行排列的。

    相较于comparable接口,comparator对类的侵入性非常弱,实现comparator接口的类如果需要更改排序方式,对于类的实现者来说,并不需要修改原本类当中的内容,只需要添加新类并重写compare()方法,而类的调用者也只需修改需要调用的类和调用方式。

Clonable接口

    Object 类中存在一个 clone 方法, 调用这个方法可以创建一个对象的 "拷贝". 但是要想合法调用 clone 方法, 必须要先实现 Clonable 接口, 否则就会抛出 CloneNotSupportedException 异常。

定义一个Person类并实现Clonable接口:

class Person implements Cloneable{//定义Person类
    String name;
    int age;
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public void eat(){
        System.out.println(this.name+" is eating");
    }
        //实现Clonable接口必须重写clone()方法
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

类的调用者只需调用clone()方法:

public class Test3 {
    public static void main(String[] args) throws CloneNotSupportedException {
        Person person1 = new Person("张三",18);
            Person person2 = (Person) person1.clone();
        System.out.println(person1.name+" "+person1.age);
        System.out.println(person2.name+" "+person2.age);
    }
}
//结果:
张三 18
张三 18

The end

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

zhanglf6699

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

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

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

打赏作者

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

抵扣说明:

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

余额充值