第6章 面向对象基础(中)

本文介绍了Java面向对象的基础知识,包括封装、继承和多态。封装通过权限修饰符控制类的访问边界,强调隐藏内部实现细节,提供公共接口。继承允许子类从父类继承特性,Java只支持单继承,但可以有多个子类,增强了代码复用和扩展性。多态是指子类可以替换父类并表现出不同的行为,允许在不确定具体类型的情况下调用方法。此外,文章还讨论了构造器、Object类的方法、final关键字、以及接口的概念和使用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

JavaSE_第6章 面向对象基础–中

教学目标

  • 理解封装的概念
  • 掌握权限修饰符的使用
  • 掌握成员变量的私有化
  • 能够写出类的继承格式
  • 能够说出继承的特点
  • 能够说出方法重写的概念以及和重载的区别
  • 能够应用多态解决问题
  • 理解向上转型与向下转型
  • 能够使用instanceof关键字判断对象类型
  • 掌握构造器的声明与使用
  • 能够使用this关键字解决问题
  • 能够使用super关键字解决问题
  • 了解实例初始化过程
  • 了解Object类的常用方法
  • 会重写Object的常用方法
  • 了解JavaBean概念

6.1 封装

6.1.1 封装概述

1、为什么需要封装?
  • 我要用洗衣机,只需要按一下开关和洗涤模式就可以了。有必要了解洗衣机内部的结构吗?有必要碰电动机吗?
  • 我们使用的电脑,内部有CPU、硬盘、键盘、鼠标等等,每一个部件通过某种连接方式一起工作,但是各个部件之间又是独立的
  • 现实生活中,每一个个体与个体之间是有边界的,每一个团体与团体之间是有边界的,而同一个个体、团体内部的信息是互通的,只是对外有所隐瞒。

面向对象编程语言是对客观世界的模拟,客观世界里每一个事物的内部信息都是隐藏在对象内部的,外界无法直接操作和修改,只能通过指定的方式进行访问和修改。封装可以被认为是一个保护屏障,防止该类的代码和数据被其他类随意访问。适当的封装可以让代码更容易理解与维护,也加强了代码的安全性。

随着我们系统越来越复杂,类会越来越多,那么类之间的访问边界必须把握好,面向对象的开发原则要遵循“高内聚、低耦合”,而“高内聚,低耦合”的体现之一:

  • 高内聚:类的内部数据操作细节自己完成,不允许外部干涉;
  • 低耦合:仅对外暴露少量的方法用于使用

隐藏对象内部的复杂性,只对外公开简单和可控的访问方式,从而提高系统的可扩展性、可维护性。通俗的讲,把该隐藏的隐藏起来,该暴露的暴露出来。这就是封装性的设计思想。

2、如何实现封装呢?

实现封装就是指控制类或成员的可见性范围?这就需要依赖访问控制修饰符,也称为权限修饰符来控制。

权限修饰符:public,protected,缺省,private

修饰符本类本包其他包子类其他包非子类
private×××
缺省××
protected×
public

外部类:public和缺省

成员变量、成员方法、构造器、成员内部类:public,protected,缺省,private

6.1.2 成员变量/属性私有化问题

成员变量(field)私有化之后,提供标准的get/set方法,我们把这种成员变量也称为属性(property)。

或者可以说只要能通过get/set操作的就是事物的属性,哪怕它没有对应的成员变量。

1、成员变量封装的目的
  • 隐藏类的实现细节
  • 让使用者只能通过事先预定的方法来访问数据,从而可以在该方法里面加入控制逻辑,限制对成员变量的不合理访问。还可以进行数据检查,从而有利于保证对象信息的完整性。
  • 便于修改,提高代码的可维护性。主要说的是隐藏的部分,在内部修改了,如果其对外可以的访问方式不变的话,外部根本感觉不到它的修改。例如:Java8->Java9,String从char[]转为byte[]内部实现,而对外的方法不变,我们使用者根本感觉不到它内部的修改。
2、实现步骤
  1. 使用 private 修饰成员变量
private 数据类型 变量名 ;

代码如下:

public class Person {
    private String name;
  	private int age;
    private boolean marry;
}
  1. 提供 getXxx方法 / setXxx 方法,可以访问成员变量,代码如下:
public class Person {
    private String name;
  	private int age;
    private boolean marry;

	public void setName(String n) {
		name = n;
    }

    public String getName() {
        return name;
	}

    public void setAge(int a) {
        age = a;
    }

    public int getAge() {
        return age;
    }
    
    public void setMarry(boolean m){
        marry = m;
    }
    
    public boolean isMarry(){
        return marry;
    }
}

3、测试

package com.atguigu.encapsulation;

public class TestPerson {
    public static void main(String[] args) {
        Person p = new Person();

        //实例变量私有化,跨类是无法直接使用的
/*        p.name = "张三";
        p.age = 23;
        p.marry = true;*/

        p.setName("张三");
        System.out.println("p.name = " + p.getName());

        p.setAge(23);
        System.out.println("p.age = " + p.getAge());

        p.setMarry(true);
        System.out.println("p.marry = " + p.isMarry());
    }
}

6.1.3 IDEA自动生成get/set方法模板

1、如何解决局部变量与实例变量同名问题

当局部变量与实例变量(非静态成员变量)同名时,在实例变量必须前面加“this.”

package com.atguigu.encapsulation;

public class Employee {
    private String name;
    private int age;
    private boolean marry;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public boolean isMarry() {
        return marry;
    }

    public void setMarry(boolean marry) {
        this.marry = marry;
    }
}
package com.atguigu.encapsulation;

public class TestEmployee {
    public static void main(String[] args) {
        Employee e = new Employee();

        e.setName("张三");
        System.out.println("e.name = " + e.getName());

        e.setAge(23);
        System.out.println("e.age = " + e.getAge());

        e.setMarry(true);
        System.out.println("e.marry = " + e.isMarry());
    }
}
2、IDEA自动生成get/set方法模板
  • 大部分键盘模式按Alt + Insert键。
  • 部分键盘模式需要按Alt + Insert + Fn键。
  • Mac电脑快捷键需要单独设置

image-20211229171605642

image-20211229171757032

6.2 继承

6.2.1 继承的概述

生活中的继承
  • 财产:富二代

  • 样貌:如图所示:

  • 才华:如图所示:

继承有延续(下一代延续上一代的基因、财富)、扩展(下一代和上一代又有所不同)的意思。

Java中的继承

如图所示:

多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中,那么多个类中无需再定义这些属性和行为,只需要和抽取出来的类构成某种关系。如图所示:

其中,多个类可以称为子类,也叫派生类;多个类抽取出来的这个类称为父类超类(superclass)或者基类

继承描述的是事物之间的所属关系,这种关系是:is-a 的关系。例如,图中猫属于动物,狗也属于动物。可见,父类更通用或更一般,子类更具体。我们通过继承,可以使多种事物之间形成一种关系体系。

继承的好处
  • 提高代码的复用性
  • 提高代码的扩展性
  • 表示类与类之间的is-a关系

6.2.2 继承的语法格式

通过 extends 关键字,可以声明一个子类继承另外一个父类,定义格式如下:

【修饰符】 class 父类 {
	...
}

【修饰符】 class 子类 extends 父类 {
	...
}

1、父类
package com.atguigu.inherited.grammar;

/*
 * 定义动物类Animal,做为父类
 */
public class Animal {
    // 定义name属性
    String name;
    // 定义age属性
    int age;

    // 定义动物的吃东西方法
    public void eat() {
        System.out.println(age + "岁的"
                + name + "在吃东西");
    }
}

2、子类
package com.atguigu.inherited.grammar;

/*
 * 定义猫类Cat 继承 动物类Animal
 */
public class Cat extends Animal {
    int count;//记录每只猫抓的老鼠数量

    // 定义一个猫抓老鼠的方法catchMouse
    public void catchMouse() {
        count++;
        System.out.println("抓老鼠,已经抓了"
                + count + "只老鼠");
    }
}
3、测试类
package com.atguigu.inherited.grammar;

public class TestCat {
    public static void main(String[] args) {
        // 创建一个猫类对象
        Cat cat = new Cat();
        // 为该猫类对象的name属性进行赋值
        cat.name = "Tom";
        // 为该猫类对象的age属性进行赋值
        cat.age = 2;
        // 调用该猫继承来的eat()方法
        cat.eat();
        // 调用该猫的catchMouse()方法
        cat.catchMouse();
        cat.catchMouse();
        cat.catchMouse();
    }
}

6.2.3 继承的特点

1.子类会继承父类所有的实例变量和实例方法

从类的定义来看,类是一类具有相同特性的事物的抽象描述。父类是所有子类共同特征的抽象描述。而实例变量和实例方法就是事物的特征,那么父类中声明的实例变量和实例方法代表子类事物也有这个特征。

  • 当子类对象被创建时,在堆中给对象申请内存时,就要看子类和父类都声明了什么实例变量,这些实例变量都要分配内存。
  • 当子类对象调用方法时,编译器会先在子类模板中看该类是否有这个方法,如果没找到,会看它的父类甚至父类的父类是否声明了这个方法,遵循从下往上找的顺序,找到了就停止,一直到根父类都没有找到,就会报编译错误。

所以继承意味着子类的对象除了看子类的类模板还要看父类的类模板。

image-20211230090255997

2.Java只支持单继承,不支持多重继承
public class A{}
class B extends A{}

//一个类只能有一个父类,不可以有多个直接父类。
class C extends B{} 	//ok
class C extends AB...	//error
3.Java支持多层继承(继承体系)
class A{}
class B extends A{}
class C extends B{}

顶层父类是Object类。所有的类默认继承Object,作为父类。

4.一个父类可以同时拥有多个子类
class A{}
class B extends A{}
class D extends A{}
class E extends A{}

6.2.4 权限修饰符限制问题

权限修饰符:public,protected,缺省,private

修饰符本类本包其他包子类其他包非子类
private×××
缺省√(本包子类非子类都可见)××
protected√(本包子类非子类都可见)√(其他包仅限于子类中可见)×
public

外部类:public和缺省

成员变量、成员方法等:public,protected,缺省,private

1、外部类要跨包使用必须是public,否则仅限于本包使用

(1)外部类的权限修饰符如果缺省,本包使用没问题

image-20211230093627763

(2)外部类的权限修饰符如果缺省,跨包使用有问题

image-20211230094236974

2、成员的权限修饰符问题

(1)本包下使用:成员的权限修饰符可以是public、protected、缺省

image-20211230095320646

(2)跨包下使用:要求严格

image-20211230095817784

(3)跨包使用时,如果类的权限修饰符缺省,成员权限修饰符>类的权限修饰符也没有意义

image-20211230100219840

3、父类成员变量私有化(private)

子类虽会继承父类私有(private)的成员变量,但子类不能对继承的私有成员变量直接进行访问,可通过继承的get/set方法进行访问。如图所示:

父类代码:

package com.atguigu.inherited.modifier;

public class Person {
    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getInfo(){
        return "姓名:" + name + ",年龄:" + age;
    }
}

子类代码:

package com.atguigu.inherited.modifier;

public class Student extends Person {
    private int score;

    public int getScore() {
        return score;
    }

    public void setScore(int score) {
        this.score = score;
    }

    public String getInfo(){
//        return "姓名:" + name + ",年龄:" + age;
        //在子类中不能直接使用父类私有的name和age
        return "姓名:" + getName() + ",年龄:" + getAge();
    }
}

测试类代码:

package com.atguigu.inherited.modifier;

public class TestStudent {
    public static void main(String[] args) {
        Student student = new Student();

        student.setName("张三");
        student.setAge(23);
        student.setScore(89);

        System.out.println(student.getInfo());
    }
}

IDEA在Debug模式下查看学生对象信息:

image-20211230101938382

6.2.5 方法重写(Override)

我们说父类的所有方法子类都会继承,但是当某个方法被继承到子类之后,子类觉得父类原来的实现不适合于子类,该怎么办呢?我们可以进行方法重写 (Override)

1、方法重写

比如新的手机增加来电显示头像的功能,代码如下:

package com.atguigu.inherited.method;

public class Phone {
    public void sendMessage(){
        System.out.println("发短信");
    }
    public void call(){
        System.out.println("打电话");
    }
    public void showNum(){
        System.out.println("来电显示号码");
    }
}

package com.atguigu.inherited.method;

//smartphone:智能手机
public class Smartphone extends Phone{
    //重写父类的来电显示功能的方法
    public void showNum(){
        //来电显示姓名和图片功能
        System.out.println("显示来电姓名");
        System.out.println("显示头像");
    }
}
package com.atguigu.inherited.method;

public class TestOverride {
    public static void main(String[] args) {
        // 创建子类对象
        Smartphone sp = new Smartphone();

        // 调用父类继承而来的方法
        sp.call();

        // 调用子类重写的方法
        sp.showNum();
    }
}
2、在子类中如何调用父类被重写的方法
package com.atguigu.inherited.method;

//smartphone:智能手机
public class Smartphone extends Phone{
    //重写父类的来电显示功能的方法
    public void showNum(){
        //来电显示姓名和图片功能
        System.out.println("显示来电姓名");
        System.out.println("显示头像");

        //保留父类来电显示号码的功能
        super.showNum();//此处必须加super.,否则就是无限递归,那么就会栈内存溢出
    }
}
3、IDEA重写方法快捷键:Ctrl + O

image-20211230104547719

package com.atguigu.inherited.method;

//smartphone:智能手机
public class Smartphone extends Phone{
    //重写父类的来电显示功能的方法
    public void showNum(){
        //来电显示姓名和图片功能
        System.out.println("显示来电姓名");
        System.out.println("显示头像");

        //保留父类来电显示号码的功能
        super.showNum();//此处必须加super.,否则就是无限递归,那么就会栈内存溢出
    }

    @Override
    public void call() {
        super.call();
        System.out.println("视频通话");
    }
}

@Override:写在方法上面,用来检测是不是满足重写方法的要求。这个注解就算不写,只要满足要求,也是正确的方法覆盖重写。建议保留,这样编译器可以帮助我们检查格式,另外也可以让阅读源代码的程序员清晰的知道这是一个重写的方法。

4、重写方法的要求

1.必须保证父子类之间重写方法的名称相同。

2.必须保证父子类之间重写方法的参数列表也完全相同。

2.子类方法的返回值类型必须【小于等于】父类方法的返回值类型(小于其实就是是它的子类,例如:Student < Person)。

注意:如果返回值类型是基本数据类型和void,那么必须是相同

3.子类方法的权限必须【大于等于】父类方法的权限修饰符。

注意:public > protected > 缺省 > private

父类私有方法不能重写

跨包的父类缺省的方法也不能重写

5、方法的重载和方法的重写

方法的重载:方法名相同,形参列表不同。不看返回值类型。

方法的重写:见上面。

(1)同一个类中

package com.atguigu.inherited.method;

public class TestOverload {
    public int max(int a, int b){
        return a > b ? a : b;
    }
    public double max(double a, double b){
        return a > b ? a : b;
    }
    public int max(int a, int b,int c){
        return max(max(a,b),c);
    }
}

(2)父子类中

package com.atguigu.inherited.method;

public class TestOverloadOverride {
    public static void main(String[] args) {
        Son s = new Son();
        s.method(1);//只有一个形式的method方法

        Daughter d = new Daughter();
        d.method(1);
        d.method(1,2);//有两个形式的method方法
    }
}

class Father{
    public void method(int i){
        System.out.println("Father.method");
    }
}
class Son extends Father{
    public void method(int i){//重写
        System.out.println("Son.method");
    }
}
class Daughter extends Father{
    public void method(int i,int j){//重载
        System.out.println("Daughter.method");
    }
}

6.2.6 实例初始化

1、普通代码块

和构造器一样,也是用于实例变量的初始化等操作。

2、普通代码块的语法格式
【修饰符】 class{
    {
        普通代码块
    }
    【修饰符】 构造器名(){
    	// 实例初始化代码
    }
    【修饰符】 构造器名(参数列表){
        // 实例初始化代码
    }
}
3、静态代码块

如果想要为静态变量初始化,可以直接在静态变量的声明后面直接赋值,也可以使用静态代码块。

1、语法格式

在代码块的前面加static,就是静态代码块。

【修饰符】 class{
	static{
        静态代码块
    }
}
2、静态代码块的特点

每一个类的静态代码块只会执行一次。

静态代码块的执行优先于非静态代码块和构造器。

package com.atguigu.keyword;

public class Chinese {
//    private static String country = "中国";

    private static String country;
    private String name;

    {
        System.out.println("非静态代码块,country = " + country);
    }

    static {
        country = "中国";
        System.out.println("静态代码块");
    }

    public Chinese(String name) {
        this.name = name;
    }
}
package com.atguigu.keyword;

public class TestStaticBlock {
    public static void main(String[] args) {
        Chinese c1 = new Chinese("张三");
        Chinese c2 = new Chinese("李四");
    }
}

4、类初始化

(1)类的初始化就是为静态变量初始化。实际上,类初始化的过程时在调用一个()方法,而这个方法是编译器自动生成的。编译器会将如下两部分的所有代码,按顺序合并到类初始化()方法体中。

  • 静态类成员变量的显式赋值语句
  • 静态代码块中的语句

(2)每个类初始化只会进行一次,如果子类初始化时,发现父类没有初始化,那么会先初始化父类。

(3)类的初始化一定优先于实例初始化。

1、类初始化代码只执行一次
package com.atguigu.keyword;

public class Fu{
    static{
        System.out.println("Fu静态代码块1,a = " + Fu.a);
    }
    private static int a = 1;
    static{
        System.out.println("Fu静态代码块2,a = " + a);
    }
    
    public static void method(){
        System.out.println("Fu.method");
    }

}
package com.atguigu.keyword;

public class TestClassInit {
    public static void main(String[] args) {
        Fu.method();
    }
}
2、父类优先于子类初始化
package com.atguigu.keyword;

public class Zi extends Fu{
    static{
        System.out.println("Zi静态代码块");
    }
}
package com.atguigu.keyword;

public class TestZiInit {
    public static void main(String[] args) {
        Zi z = new Zi();
    }
}

3、类初始化优先于实例初始化

package com.atguigu.keyword;

public class Fu{
    static{
        System.out.println("Fu静态代码块1,a = " + Fu.a);
    }
    private static int a = 1;
    static{
        System.out.println("Fu静态代码块2,a = " + a);
    }

    {
        System.out.println("Fu非静态代码块");
    }
    public Fu(){
        System.out.println("Fu构造器");
    }

    public static void method(){
        System.out.println("Fu.method");
    }
}

package com.atguigu.keyword;

public class Zi extends Fu{
    static{
        System.out.println("Zi静态代码块");
    }
    {
        System.out.println("Zi非静态代码块");
    }
    public Zi(){
        System.out.println("Zi构造器");
    }
}
package com.atguigu.keyword;

public class TestZiInit {
    public static void main(String[] args) {
        Zi z1 = new Zi();
        Zi z2 = new Zi();
    }
}

6.2.7 Object根父类

1、如何理解根父类

java.lang.Object是类层次结构的根类,即所有类的父类。每个类都使用 Object 作为超类。

  • Object类型的变量与除Object以外的任意引用数据类型的对象都多态引用
  • 所有对象(包括数组)都实现这个类的方法。
  • 如果一个类没有特别指定父类,那么默认则继承自Object类。例如:
public class MyClass /*extends Object*/ {
  	// ...
}
2、Object类的其中5个方法

API(Application Programming Interface),应用程序编程接口。Java API是一本程序员的字典 ,是JDK中提供给我们使用的类的说明文档。所以我们可以通过查询API的方式,来学习Java提供的类,并得知如何使用它们。在API文档中是无法得知这些类具体是如何实现的,如果要查看具体实现代码,那么我们需要查看src源码

根据JDK源代码及Object类的API文档,Object类当中包含的方法有11个。今天我们主要学习其中的5个:

(1)toString()

方法签名:public String toString()

①默认情况下,toString()返回的是“对象的运行时类型 @ 对象的hashCode值的十六进制形式"

②通常是建议重写

③如果我们直接System.out.println(对象),默认会自动调用这个对象的toString()

因为Java的引用数据类型的变量中存储的实际上时对象的内存地址,但是Java对程序员隐藏内存地址信息,所以不能直接将内存地址显示出来,所以当你打印对象时,JVM帮你调用了对象的toString()。

例如自定义的Person类:

public class Person {  
    private String name;
    private int age;

    @Override
    public String toString() {
        return "Person{" + "name='" + name + '\'' + ", age=" + age + '}';
    }
}
(2)getClass()

public final Class<?> getClass():获取对象的运行时类型

因为Java有多态现象,所以一个引用数据类型的变量的编译时类型与运行时类型可能不一致,因此如果需要查看这个变量实际指向的对象的类型,需要用getClass()方法

	public static void main(String[] args) {
		Object obj = new Person();
		System.out.println(obj.getClass());//运行时类型
	}
(3)equals()

public boolean equals(Object obj):用于判断当前对象this与指定对象obj是否“相等”

①默认情况下,equals方法的实现等价于与“==”,比较的是对象的地址值

②我们可以选择重写,重写有些要求:

A:

B:如果重写equals,那么一定要遵循如下几个原则:

​ a:自反性:x.equals(x)返回true

​ b:传递性:x.equals(y)为true, y.equals(z)为true,然后x.equals(z)也应该为true

​ c:一致性:只要参与equals比较的属性值没有修改,那么无论何时调用结果应该一致

​ d:对称性:x.equals(y)与y.equals(x)结果应该一样

​ e:非空对象与null的equals一定是false

class User{
	private String host;
	private String username;
	private String password;
	public User(String host, String username, String password) {
		super();
		this.host = host;
		this.username = username;
		this.password = password;
	}
	public User() {
		super();
	}
	public String getHost() {
		return host;
	}
	public void setHost(String host) {
		this.host = host;
	}
	public String getUsername() {
		return username;
	}
	public void setUsername(String username) {
		this.username = username;
	}
	public String getPassword() {
		return password;
	}
	public void setPassword(String password) {
		this.password = password;
	}
	@Override
	public String toString() {
		return "User [host=" + host + ", username=" + username + ", password=" + password + "]";
	}
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((host == null) ? 0 : host.hashCode());
		result = prime * result + ((password == null) ? 0 : password.hashCode());
		result = prime * result + ((username == null) ? 0 : username.hashCode());
		return result;
	}
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		User other = (User) obj;
		if (host == null) {
			if (other.host != null)
				return false;
		} else if (!host.equals(other.host))
			return false;
		if (password == null) {
			if (other.password != null)
				return false;
		} else if (!password.equals(other.password))
			return false;
		if (username == null) {
			if (other.username != null)
				return false;
		} else if (!username.equals(other.username))
			return false;
		return true;
	}
	
}
(4)hashCode()

public int hashCode():返回每个对象的hash值。

如果重写equals,那么通常会一起重写hashCode()方法,hashCode()方法主要是为了当对象存储到哈希表(后面集合章节学习)等容器中时提高存储和查询性能用的,这是因为关于hashCode有两个常规协定:

  • ①如果两个对象的hash值是不同的,那么这两个对象一定不相等;
  • ②如果两个对象的hash值是相同的,那么这两个对象不一定相等。

重写equals和hashCode方法时,要保证满足如下要求:

  • ①如果两个对象调用equals返回true,那么要求这两个对象的hashCode值一定是相等的;
  • ②如果两个对象的hashCode值不同的,那么要求这个两个对象调用equals方法一定是false;
  • ③如果两个对象的hashCode值相同的,那么这个两个对象调用equals可能是true,也可能是false
	public static void main(String[] args) {
		System.out.println("Aa".hashCode());//2112
		System.out.println("BB".hashCode());//2112
	}
(5)finalize()

protected void finalize():用于最终清理内存的方法

面试题:对finalize()的理解?

  • 当对象被GC确定为要被回收的垃圾,在回收之前由GC帮你调用这个方法,不是由程序员手动调用。
  • 这个方法与C语言的析构函数不同,C语言的析构函数被调用,那么对象一定被销毁,内存被回收,而finalize方法的调用不一定会销毁当前对象,因为可能在finalize()中出现了让当前对象“复活”的代码
  • 每一个对象的finalize方法只会被调用一次。
  • 子类可以选择重写,一般用于彻底释放一些资源对象,而且这些资源对象往往时通过C/C++等代码申请的资源内存
(6)重写toString和equals
3、标准JavaBean

JavaBean 是 Java语言编写类的一种标准规范。符合JavaBean 的类,要求:

(1)类必须是具体的和公共的,

(2)并且具有无参数的构造方法,

(3)成员变量私有化,并提供用来操作成员变量的setget 方法。

(4)重写toString方法

public class ClassName{
  //成员变量
    
  //构造方法
  	//无参构造方法【必须】
  	//有参构造方法【建议】
  	
  //getXxx()
  //setXxx()
  //其他成员方法
}

编写符合JavaBean 规范的类,以学生类为例,标准代码如下:

public class Student {
	// 成员变量
	private String name;
	private int age;

	// 构造方法
	public Student() {
	}

	public Student(String name, int age) {
		this.name = name;
		this.age = age;
	}

	// get/set成员方法
	public void setName(String name) {
		this.name = name;
	}

	public String getName() {
		return name;
	}

	public void setAge(int age) {
		this.age = age;
	}

	public int getAge() {
		return age;
	}
    
    //其他成员方法列表
    public String toString(){
        return "姓名:" + name + ",年龄:" + age;
    }
}

测试类,代码如下:

public class TestStudent {
	public static void main(String[] args) {
		// 无参构造使用
		Student s = new Student();
		s.setName("柳岩");
		s.setAge(18);
		System.out.println(s.getName() + "---" + s.getAge());
        System.out.println(s);

		// 带参构造使用
		Student s2 = new Student("赵丽颖", 18);
		System.out.println(s2.getName() + "---" + s2.getAge());
        System.out.println(s2);
	}
}

6.2.8 final关键字

1、final的意义

final:最终的,不可更改的

2、final修饰类

表示这个类不能被继承,没有子类

final class Eunuch{//太监类
	
}
class Son extends Eunuch{//错误
	
}
3、final修饰方法

表示这个方法不能被子类重写

class Father{
	public final void method(){
		System.out.println("father");
	}
}
class Son extends Father{
	public void method(){//错误
		System.out.println("son");
	}
}
4、final修饰变量

final修饰某个变量(成员变量或局部变量),表示它的值就不能被修改,即常量,常量名建议使用大写字母。

如果某个成员变量用final修饰后,没有set方法,并且必须初始化(可以显式赋值、或在初始化块赋值、实例变量还可以在构造器中赋值)

package com.atguigu.keyword.finals;

public class TestFinal {
    public static void main(String[] args){
        final int MIN_SCORE = 0;
        final int MAX_SCORE = 100;

        MyDate m1 = new MyDate();
        System.out.println(m1.getInfo());

        MyDate m2 = new MyDate(2022,2,14);
        System.out.println(m2.getInfo());
    }
}
class MyDate{
    //没有set方法,必须有显示赋值的代码
    private final int year;
    private final int month;
    private final int day;

    public MyDate(){
        year = 1970;
        month = 1;
        day = 1;
    }

    public MyDate(int year, int month, int day) {
        this.year = year;
        this.month = month;
        this.day = day;
    }

    public int getYear() {
        return year;
    }

    public int getMonth() {
        return month;
    }

    public int getDay() {
        return day;
    }

    public String getInfo(){
        return year + "年" + month + "月" + day + "日";
    }
}

6.3 多态

多态是继封装、继承之后,面向对象的第三大特性。

生活中,比如求面积的功能,圆、矩形、三角形实现起来是不一样的。跑的动作,小猫、小狗和大象,跑起来是不一样的。再比如飞的动作,昆虫、鸟类和飞机,飞起来也是不一样的。可见,同一行为,通过不同的事物,可以体现出来的不同的形态。那么此时就会出现各种子类的类型。

6.3.1 多态解决什么样的问题

有的时候,我们在设计一个数组、或一个成员变量、或一个方法的形参、返回值类型时,无法确定它具体的类型,只能确定它是某个系列的类型。

案例:

(1)声明一个Dog类,包含public void eat()方法,输出“狗狗啃骨头”

(2)声明一个Cat类,包含public void eat()方法,输出“猫咪吃鱼仔”

(3)声明一个Person类,

  • 包含宠物属性
  • 包含领养宠物方法 public void adopt(宠物类型 pet)
  • 包含喂宠物吃东西的方法 public void feed(),实现为调用宠物对象.eat()方法
package com.atguigu.polymorphism.problem;

public class Dog {
    public void eat(){
        System.out.println("狗狗啃骨头");
    }
}
package com.atguigu.polymorphism.problem;

public class Cat {
    public void eat(){
        System.out.println("猫咪吃鱼仔");
    }
}
package com.atguigu.polymorphism.problem;

public class Person {
    private Dog dog;

    //adopt:领养
    public void adopt(Dog dog){
        this.dog = dog;
    }

    //feed:喂食
    public void feed(){
        if(dog != null){
            dog.eat();
        }
    }
    /*
    问题:
    1、从养狗切换到养猫怎么办?   
    	修改代码把Dog修改为养猫?
    2、或者有的人养狗,有的人养猫怎么办?  
    3、要是同时养多个狗,或猫怎么办?
    4、要是还有更多其他宠物类型怎么办?
    如果Java不支持多态,那么上面的问题将会非常麻烦,代码维护起来很难,扩展性很差。
    */
}

6.3.2 多态的形式和体现

1、多态引用

Java规定父类类型的变量可以接收子类类型的对象,这一点从逻辑上也是说得通的。

父类类型 变量名 = 子类对象;

父类类型:指子类继承的父类类型,或者实现的父接口类型。

所以说继承是多态的前提

2、多态引用的表现

表现:编译时类型与运行时类型不一致,编译时看“父类”,运行时看“子类”。

3、多态引用的好处和弊端

弊端:编译时,只能调用父类声明的方法,不能调用子类扩展的方法;

好处:运行时,看“子类”,如果子类重写了方法,一定是执行子类重写的方法体;变量引用的子类对象不同,执行的方法就不同,实现动态绑定。代码编写更灵活、功能更强大,可维护性和扩展性更好了。

4、多态演示

让Dog和Cat都继承Pet宠物类。

package com.atguigu.polymorphism.grammar;

public class Pet {
    private String nickname;

    public String getNickname() {
        return nickname;
    }

    public void setNickname(String nickname) {
        this.nickname = nickname;
    }

    public void eat(){
        System.out.println(nickname + "吃东西");
    }
}
package com.atguigu.polymorphism.grammar;

public class Cat extends Pet {
    //子类重写父类的方法
    @Override
    public void eat() {
        System.out.println("猫咪" + getNickname() + "吃鱼仔");
    }

    //子类扩展的方法
    public void catchMouse() {
        System.out.println("抓老鼠");
    }
}
package com.atguigu.polymorphism.grammar;

public class Dog extends Pet {
    //子类重写父类的方法
    @Override
    public void eat() {
        System.out.println("狗狗" + getNickname() + "啃骨头");
    }

    //子类扩展的方法
    public void watchHouse() {
        System.out.println("看家");
    }
}
package com.atguigu.polymorphism.grammar;

public class TestPet {
    public static void main(String[] args) {
        //多态引用
        Pet pet = new Dog();
        pet.setNickname("小白");

        //多态的表现形式
        /*
        编译时看父类:只能调用父类声明的方法,不能调用子类扩展的方法;
        运行时,看“子类”,如果子类重写了方法,一定是执行子类重写的方法体;
         */
        pet.eat();//运行时执行子类Dog重写的方法
//        pet.watchHouse();//不能调用Dog子类扩展的方法

        pet = new Cat();
        pet.setNickname("雪球");
        pet.eat();//运行时执行子类Cat重写的方法
    }
}

6.3.3 应用多态解决问题

1、声明变量是父类类型,变量赋值子类对象
  • 方法的形参是父类类型,调用方法的实参是子类对象
  • 实例变量声明父类类型,实际存储的是子类对象
package com.atguigu.polymorphism.grammar;

public class OnePersonOnePet {
    private Pet pet;
    public void adopt(Pet pet) {//形参是父类类型,实参是子类对象
        this.pet = pet;
    }
    public void feed(){
        pet.eat();//pet实际引用的对象类型不同,执行的eat方法也不同
    }
}
package com.atguigu.polymorphism.grammar;

public class TestOnePersonOnePet {
    public static void main(String[] args) {
        OnePersonOnePet person = new OnePersonOnePet();

        Dog dog = new Dog();
        dog.setNickname("小白");
        person.adopt(dog);//实参是dog子类对象,形参是父类Pet类型
        person.feed();

        Cat cat = new Cat();
        cat.setNickname("雪球");
        person.adopt(cat);//实参是cat子类对象,形参是父类Pet类型
        person.feed();
    }
}
2、数组元素是父类类型,元素对象是子类对象
package com.atguigu.polymorphism.grammar;

public class OnePersonManyPets {
    private Pet[] pets;//数组元素类型是父类类型,元素存储的是子类对象

    public void adopt(Pet[] pets) {
        this.pets = pets;
    }

    public void feed() {
        for (int i = 0; i < pets.length; i++) {
            pets[i].eat();//pets[i]实际引用的对象类型不同,执行的eat方法也不同
        }
    }
}
package com.atguigu.polymorphism.grammar;

public class TestPets {
    public static void main(String[] args) {
        Pet[] pets = new Pet[2];
        pets[0] = new Dog();//多态引用
        pets[0].setNickname("小白");
        pets[1] = new Cat();//多态引用
        pets[1].setNickname("雪球");

        OnePersonManyPets person = new OnePersonManyPets();
        person.adopt(pets);
        person.feed();
    }
}
3、方法返回值类型声明为父类类型,实际返回的是子类对象
package com.atguigu.polymorphism.grammar;

public class PetShop {
    //返回值类型是父类类型,实际返回的是子类对象
    public Pet sale(String type){
        switch (type){
            case "Dog":
                return new Dog();
            case "Cat":
                return new Cat();
        }
        return null;
    }
}
package com.atguigu.polymorphism.grammar;

public class TestPetShop {
    public static void main(String[] args) {
        PetShop shop = new PetShop();

        Pet dog = shop.sale("Dog");
        dog.setNickname("小白");
        dog.eat();

        Pet cat = shop.sale("Cat");
        cat.setNickname("雪球");
        cat.eat();
    }
}

6.3.4 向上转型与向下转型

首先,一个对象在new的时候创建是哪个类型的对象,它从头至尾都不会变。即这个对象的运行时类型,本质的类型用于不会变。但是,把这个对象赋值给不同类型的变量时,这些变量的编译时类型却不同。

这个和基本数据类型的转换是不同的。基本数据类型是把数据值copy了一份,相当于有两种数据类型的值。而对象的赋值不会产生两个对象。

1、为什么要类型转换呢?

因为多态,就一定会有把子类对象赋值给父类变量的时候,这个时候,在编译期间,就会出现类型转换的现象。

但是,使用父类变量接收了子类对象之后,我们就不能调用子类拥有,而父类没有的方法了。这也是多态给我们带来的一点"小麻烦"。所以,想要调用子类特有的方法,必须做类型转换,使得编译通过

  • 向上转型:当左边的变量的类型(父类) > 右边对象/变量的类型(子类),我们就称为向上转型

    • 此时,编译时按照左边变量的类型处理,就只能调用父类中有的变量和方法,不能调用子类特有的变量和方法了
    • 但是,运行时,仍然是对象本身的类型,所以执行的方法是子类重写的方法体。
    • 此时,一定是安全的,而且也是自动完成的
  • 向下转型:当左边的变量的类型(子类)<右边对象/变量的编译时类型(父类),我们就称为向下转型

    • 此时,编译时按照左边变量的类型处理,就可以调用子类特有的变量和方法了
    • 但是,运行时,仍然是对象本身的类型
    • 不是所有通过编译的向下转型都是正确的,可能会发生ClassCastException,为了安全,可以通过isInstanceof关键字进行判断
2、如何向上转型与向下转型

向上转型:自动完成

向下转型:(子类类型)父类变量

package com.atguigu.polymorphism.grammar;

public class ClassCastTest {
    public static void main(String[] args) {
        //没有类型转换
        Dog dog = new Dog();//dog的编译时类型和运行时类型都是Dog

        //向上转型
        Pet pet = new Dog();//pet的编译时类型是Pet,运行时类型是Dog
        pet.setNickname("小白");
        pet.eat();//可以调用父类Pet有声明的方法eat,但执行的是子类重写的eat方法体
//        pet.watchHouse();//不能调用父类没有的方法watchHouse

        Dog d = (Dog) pet;
        System.out.println("d.nickname = " + d.getNickname());
        d.eat();//可以调用eat方法
        d.watchHouse();//可以调用子类扩展的方法watchHouse

        Cat c = (Cat) pet;//编译通过,因为从语法检查来说,pet的编译时类型是Pet,Cat是Pet的子类,所以向下转型语法正确
        //这句代码运行报错ClassCastException,因为pet变量的运行时类型是Dog,Dog和Cat之间是没有继承关系的
    }
}
3、instanceof关键字

为了避免ClassCastException的发生,Java提供了 instanceof 关键字,给引用变量做类型的校验,只要用instanceof判断返回true的,那么强转为该类型就一定是安全的,不会报ClassCastException异常。

变量/匿名对象 instanceof 数据类型 

那么,哪些instanceof判断会返回true呢?

  • 变量/匿名对象的编译时类型 与 instanceof后面数据类型是直系亲属关系才可以比较
  • 变量/匿名对象的运行时类型<= instanceof后面数据类型,才为true

示例代码:

package com.atguigu.polymorphism.grammar;

public class TestInstanceof {
    public static void main(String[] args) {
        Pet[] pets = new Pet[2];
        pets[0] = new Dog();//多态引用
        pets[0].setNickname("小白");
        pets[1] = new Cat();//多态引用
        pets[1].setNickname("雪球");

        for (int i = 0; i < pets.length; i++) {
            pets[i].eat();

            if(pets[i] instanceof Dog){
                Dog dog = (Dog) pets[i];
                dog.watchHouse();
            }else if(pets[i] instanceof Cat){
                Cat cat = (Cat) pets[i];
                cat.catchMouse();
            }
        }
    }
}

6.3.5 虚方法

在Java中虚方法是指在编译阶段和类加载阶段都不能确定方法的调用入口地址,在运行阶段才能确定的方法,即可能被重写的方法。

当我们通过“对象xx.方法”的形式调用一个虚方法时,要如何确定它具体执行哪个方法呢?

(1)静态分派:先看这个对象xx的编译时类型,在这个对象的编译时类型中找到能匹配的方法

匹配的原则:看实参的编译时类型与方法形参的类型的匹配程度
A:找最匹配    实参的编译时类型 = 方法形参的类型
B:找兼容      实参的编译时类型 < 方法形参的类型

(2)动态绑定:再看这个对象xx的运行时类型,如果这个对象xx的运行时类重写了刚刚找到的那个匹配的方法,那么执行重写的,否则仍然执行刚才编译时类型中的那个匹配的方法

6.3.6 成员变量没有多态一说

package com.atguigu.polymorphism.grammar;

public class TestVariable {
    public static void main(String[] args) {
        Base b = new Sub();
        System.out.println(b.a);
        System.out.println(((Sub)b).a);

        Sub s = new Sub();
        System.out.println(s.a);
        System.out.println(((Base)s).a);
    }
}
class Base{
    int a = 1;
}
class Sub extends Base{
    int a = 2;
}

6.4 抽象类

6.4.1 由来

抽象:即不具体、或无法具体

例如:当我们声明一个几何图形类:圆、矩形、三角形类等,发现这些类都有共同特征:求面积、求周长、获取图形详细信息。那么这些共同特征应该抽取到一个公共父类中。但是这些方法在父类中又无法给出具体的实现,而是应该交给子类各自具体实现。那么父类在声明这些方法时,就只有方法签名,没有方法体,我们把没有方法体的方法称为抽象方法。Java语法规定,包含抽象方法的类必须是抽象类

6.4.2 语法格式

  • 抽象方法:被abstract修饰没有方法体的方法。
  • 抽象类:被abstract修饰的类。

抽象类的语法格式

【权限修饰符】 abstract class 类名{
    
}
【权限修饰符】 abstract class 类名 extends 父类{
    
}

抽象方法的语法格式

【其他修饰符】 abstract 返回值类型 方法名(【形参列表】);

注意:抽象方法没有方法体

代码举例:

public abstract class Animal {
    public abstract void eat()}
public class Cat extends Animal {
    public void run (){
      	System.out.println("小猫吃鱼和猫粮")}
}
public class CatTest {
 	 public static void main(String[] args) {
        // 创建子类对象
        Cat c = new Cat(); 
       
        // 调用eat方法
        c.eat();
  	}
}

此时的方法重写,是子类对父类抽象方法的完成实现,我们将这种方法重写的操作,也叫做实现方法

6.4.3 注意事项

关于抽象类的使用,以下为语法上要注意的细节,虽然条目较多,但若理解了抽象的本质,无需死记硬背。

  1. 抽象类不能创建对象,如果创建,编译无法通过而报错。只能创建其非抽象子类的对象。

    理解:假设创建了抽象类的对象,调用抽象的方法,而抽象方法没有具体的方法体,没有意义。

  2. 抽象类中,也有构造方法,是供子类创建对象时,初始化父类成员变量使用的。

    理解:子类的构造方法中,有默认的super()或手动的super(实参列表),需要访问父类构造方法。

  3. 抽象类中,不一定包含抽象方法,但是有抽象方法的类必定是抽象类。

    理解:未包含抽象方法的抽象类,目的就是不想让调用者创建该类对象,通常用于某些特殊的类结构设计。

  4. 抽象类的子类,必须重写抽象父类中所有的抽象方法,否则,编译无法通过而报错。除非该子类也是抽象类。

    理解:假设不重写所有抽象方法,则类中可能包含抽象方法。那么创建对象后,调用抽象的方法,没有意义。

6.5 接口

6.5.1 概述

生活中大家每天都在用USB接口,那么USB接口与我们今天要学习的接口有什么相同点呢?

USB是通用串行总线的英文缩写,是Intel公司开发的总线架构,使得在计算机上添加串行设备(鼠标、键盘、打印机、扫描仪、摄像头、充电器、MP3机、手机、数码相机、移动硬盘等)非常容易。只须将设备插入计算机的USB端口中,系统会自动识别和配置。 有了USB,我们电脑需要提供的各种插槽的口越来越少,而能支持的其他设备的连接却越来越多。

​ 那么我们平时看到的电脑上的USB插口、以及其他设备上的USB插口是什么呢?

​ 其实,不管是电脑上的USB插口,还是其他设备上的USB插口都只是遵循了USB规范的一种具体设备而已。

​ 根据时代发展,USB接口标准经历了一代USB、第二代USB 2.0和第三代USB 3.0 。

​ USB规格第一次是于1995年,由Intel、IBM、Compaq、Microsoft、NEC、Digital、North Telecom等七家公司组成的USBIF(USB Implement Forum)共同提出,USBIF于1996年1月正式提出USB1.0规格,频宽为1.5Mbps。

USB2.0技术规范是有由Compaq、Hewlett Packard、Intel、Lucent、Microsoft、NEC、Philips共同制定、发布的,规范把外设数据传输速度提高到了480Mbps,被称为USB 2.0的高速(High-speed)版本.

USB 3.0是最新的USB规范,该规范由英特尔等公司发起,USB3.0的最大传输带宽高达5.0Gbps(640MB/s),USB3.0 引入全双工数据传输。5根线路中2根用来发送数据,另2根用来接收数据,还有1根是地线。也就是说,USB 3.0可以同步全速地进行读写操作。

USB版本最大传输速率速率称号最大输出电流推出时间
USB1.01.5Mbps(192KB/s)低速(Low-Speed)5V/500mA1996年1月
USB1.112Mbps(1.5MB/s)全速(Full-Speed)5V/500mA1998年9月
USB2.0480Mbps(60MB/s)高速(High-Speed)5V/500mA2000年4月
USB3.05Gbps(500MB/s)超高速(Super-Speed)5V/900mA2008年11月
USB 3.110Gbps(1280MB/s)超高速+(Super-speed+)20V/5A2013年12月

电脑边上提供了USB插槽,这个插槽遵循了USB的规范,只要其他设备也是遵循USB规范的,那么就可以互联,并正常通信。至于这个电脑、以及其他设备是哪个厂家制造的,内部是如何实现的,我们都无需关心。

这种设计是将规范和实现分离,这也正是Java接口的好处。Java的软件系统会有很多模块组成,那么各个模块之间也应该采用这种面相接口的低耦合,为系统提供更好的可扩展性和可维护性。

  • 接口就是规范,定义的是一组规则,体现了现实世界中“如果你是/要…则必须能…”的思想。继承是一个"是不是"的is-a关系,而接口实现则是 "能不能"的has-a关系。
    • 例如:你能不能用USB进行连接,或是否具备USB通信功能,就看你是否遵循USB接口规范
    • 例如:Java程序是否能够连接使用某种数据库产品,那么要看该数据库产品有没有实现Java设计的JDBC规范

6.5.2 定义格式

接口的定义,它与定义类方式相似,但是使用 interface 关键字。它也会被编译成.class文件,但一定要明确它并不是类,而是另外一种引用数据类型。

引用数据类型:数组,类,枚举,接口,注解。

1、接口的声明格式
【修饰符】 interface 接口名{
    //接口的成员列表:
    // 公共的静态常量
    // 公共的抽象方法
    // 公共的默认方法(JDK1.8以上)
    // 公共的静态方法(JDK1.8以上)
    // 私有方法(JDK1.9以上)
}

示例代码:

package com.atguigu.interfacetype;

public interface Usb3{
    //静态常量
    long MAX_SPEED = 500*1024*1024;//500MB/s

    //抽象方法
    void in();
    void out();

    //默认方法
    default void start(){
        System.out.println("开始");
    }
    default void stop(){
        System.out.println("结束");
    }

    //静态方法
    static void show(){
        System.out.println("USB 3.0可以同步全速地进行读写操作");
    }
}
2、接口的成员说明

接口定义的是多个类共同的公共行为规范,这些行为规范是与外部交流的通道,这就意味着接口里通常是定义一组公共方法。

在JDK8之前,接口中只允许出现:

(1)公共的静态的常量:其中public static final可以省略

(2)公共的抽象的方法:其中public abstract可以省略

理解:接口是从多个相似类中抽象出来的规范,不需要提供具体实现

在JDK1.8时,接口中允许声明默认方法和静态方法:

(3)公共的默认的方法:其中public 可以省略,建议保留,但是default不能省略

(4)公共的静态的方法:其中public 可以省略,建议保留,但是static不能省略

在JDK1.9时,接口又增加了:

(5)私有方法

除此之外,接口中不能有其他成员,没有构造器,没有初始化块,因为接口中没有成员变量需要动态初始化。

6.5.3 接口的使用

1、使用接口的静态成员

接口不能直接创建对象,但是可以通过接口名直接调用接口的静态方法和静态常量。

package com.atguigu.interfacetype;

public class TestUsb3 {
    public static void main(String[] args) {
        //通过“接口名.”调用接口的静态方法
        Usb3.show();
        //通过“接口名.”直接使用接口的静态常量
        System.out.println(Usb3.MAX_SPEED);
    }
}
2、类实现接口(implements)

接口不能创建对象,但是可以被类实现(implements ,类似于被继承)。

类与接口的关系为实现关系,即类实现接口,该类可以称为接口的实现类,也可以称为接口的子类。实现的动作类似继承,格式相仿,只是关键字不同,实现使用 implements关键字。

【修饰符】 class 实现类  implements 接口{
	// 重写接口中抽象方法【必须】,当然如果实现类是抽象类,那么可以不重写
  	// 重写接口中默认方法【可选】
}

【修饰符】 class 实现类 extends 父类 implements 接口{
    // 重写接口中抽象方法【必须】,当然如果实现类是抽象类,那么可以不重写
  	// 重写接口中默认方法【可选】
}

注意:

  1. 如果接口的实现类是非抽象类,那么必须重写接口中所有抽象方法

  2. 默认方法可以选择保留,也可以重写。

    重写时,default单词就不要再写了,它只用于在接口中表示默认方法,到类中就没有默认方法的概念了

  3. 接口中的静态方法不能被继承也不能被重写

示例代码:

package com.atguigu.interfacetype;

public class MobileHDD implements Usb3 {
    //重写/实现接口的抽象方法,【必选】
    public void out() {
        System.out.println("读取数据并发送");
    }
    public void in(){
        System.out.println("接收数据并写入");
    }

    //重写接口的默认方法,【可选】
    //重写默认方法时,default单词去掉
    public void end(){
        System.out.println("清理硬盘中的隐藏回收站中的东西,再结束");
    }
}

3、使用接口的非静态方法
  • 对于接口的静态方法,直接使用“接口名.”进行调用即可
    • 也只能使用“接口名."进行调用,不能通过实现类的对象进行调用
  • 对于接口的抽象方法、默认方法,只能通过实现类对象才可以调用
    • 接口不能直接创建对象,只能创建实现类的对象
package com.atguigu.interfacetype;

public class TestMobileHDD {
    public static void main(String[] args) {
        //创建实现类对象
        MobileHDD b = new MobileHDD();

        //通过实现类对象调用重写的抽象方法,以及接口的默认方法,如果实现类重写了就执行重写的默认方法,如果没有重写,就执行接口中的默认方法
        b.start();
        b.in();
        b.stop();

        //通过接口名调用接口的静态方法
//        MobileHDD.show();
//        b.show();
        Usb3.show();
    }
}
4、接口的多实现(implements)

之前学过,在继承体系中,一个类只能继承一个父类。而对于接口而言,一个类是可以实现多个接口的,这叫做接口的多实现。并且,一个类能继承一个父类,同时实现多个接口。

实现格式:

【修饰符】 class 实现类  implements 接口1,接口2,接口3。。。{
	// 重写接口中所有抽象方法【必须】,当然如果实现类是抽象类,那么可以不重写
  	// 重写接口中默认方法【可选】
}

【修饰符】 class 实现类 extends 父类 implements 接口1,接口2,接口3。。。{
    // 重写接口中所有抽象方法【必须】,当然如果实现类是抽象类,那么可以不重写
  	// 重写接口中默认方法【可选】
}

接口中,有多个抽象方法时,实现类必须重写所有抽象方法。如果抽象方法有重名的,只需要重写一次

定义多个接口:

package com.atguigu.interfacetype;

public interface A {
    void showA();
    void show();
}
package com.atguigu.interfacetype;

public interface B extends A {
    void showB();
    void show();
}

定义实现类:

package com.atguigu.interfacetype;

public class C implements A,B {
    @Override
    public void showA() {
        System.out.println("showA");
    }

    @Override
    public void showB() {
        System.out.println("showB");
    }

    @Override
    public void show() {
        System.out.println("show");
    }
}

测试类

package com.atguigu.interfacetype;

public class TestC {
    public static void main(String[] args) {
        C c = new C();
        c.showA();
        c.showB();
        c.show();
    }
}

5、接口的多继承 (extends)

一个接口能继承另一个或者多个接口,接口的继承也使用 extends 关键字,子接口继承父接口的方法。

定义父接口:

package com.atguigu.interfacetype;

public interface Chargeable {
    void charge();
    void in();
    void out();
}

定义子接口:

package com.atguigu.interfacetype;

public interface UsbC extends Chargeable,Usb3 {
    void reverse();
}

定义子接口的实现类:

package com.atguigu.interfacetype;

public class TypeCConverter implements UsbC {
    @Override
    public void reverse() {
        System.out.println("正反面都支持");
    }

    @Override
    public void charge() {
        System.out.println("可充电");
    }

    @Override
    public void in() {
        System.out.println("接收数据");
    }

    @Override
    public void out() {
        System.out.println("输出数据");
    }
}

所有父接口的抽象方法都有重写。

方法签名相同的抽象方法只需要实现一次。

6、接口与实现类对象构成多态引用

实现类实现接口,类似于子类继承父类,因此,接口类型的变量与实现类的对象之间,也可以构成多态引用。通过接口类型的变量调用方法,最终执行的是你new的实现类对象实现的方法体。

接口的不同实现类:

package com.atguigu.interfacetype;

public class Mouse implements Usb3 {
    @Override
    public void out() {
        System.out.println("发送脉冲信号");
    }

    @Override
    public void in() {
        System.out.println("不接收信号");
    }
}
package com.atguigu.interfacetype;

public class KeyBoard implements Usb3{
    @Override
    public void in() {
        System.out.println("不接收信号");
    }

    @Override
    public void out() {
        System.out.println("发送按键信号");
    }
}

测试类

package com.atguigu.interfacetype;

public class TestComputer {
    public static void main(String[] args) {
        Computer computer = new Computer();
        Usb3 usb = new Mouse();
        computer.setUsb(usb);
        usb.start();
        usb.out();
        usb.in();
        usb.stop();
        System.out.println("--------------------------");

        usb = new KeyBoard();
        computer.setUsb(usb);
        usb.start();
        usb.out();
        usb.in();
        usb.stop();
        System.out.println("--------------------------");

        usb = new MobileHDD();
        computer.setUsb(usb);
        usb.start();
        usb.out();
        usb.in();
        usb.stop();
    }
}

6.5.4 冲突问题

1、默认方法冲突问题
(1)亲爹优先原则

当一个类,既继承一个父类,又实现若干个接口时,父类中的成员方法与接口中的抽象方法重名,子类就近选择执行父类的成员方法。代码如下:

定义接口:

package com.atguigu.interfacetype;

public interface Friend {
    default void date(){//约会
        System.out.println("吃喝玩乐");
    }
}

定义父类:

package com.atguigu.interfacetype;

public class Father {
    public void date(){//约会
        System.out.println("爸爸约吃饭");
    }
}

定义子类:

package com.atguigu.interfacetype;

public class Son extends Father implements Friend {
    @Override
    public void date() {
        //(1)不重写默认保留父类的
        //(2)调用父类被重写的
//        super.date();
        //(3)保留父接口的
//        Friend.super.date();
        //(4)完全重写
        System.out.println("学Java");
    }
}

定义测试类:

package com.atguigu.interfacetype;

public class TestSon {
    public static void main(String[] args) {
        Son s = new Son();
        s.date();
    }
}
(2)左右为难
  • 当一个类同时实现了多个父接口,而多个父接口中包含方法签名相同的默认方法时,怎么办呢?

无论你多难抉择,最终都是要做出选择的。

声明接口:

package com.atguigu.interfacetype;

public interface BoyFriend {
    default void date(){//约会
        System.out.println("神秘约会");
    }
}

public interface Friend {
    default void date(){//约会
        System.out.println("吃喝玩乐");
    }
}

选择保留其中一个,通过“接口名.super.方法名"的方法选择保留哪个接口的默认方法。

package com.atguigu.interfacetype;

public class Girl implements Friend,BoyFriend{

    @Override
    public void date() {
        //(1)保留其中一个父接口的
//        Friend.super.date();
//        BoyFriend.super.date();
        //(2)完全重写
        System.out.println("学Java");
    }

}

测试类

package com.atguigu.interfacetype;

public class TestGirl {
    public static void main(String[] args) {
        Girl girl = new Girl();
        girl.date();
    }
}

6.5.4 接口的特点总结

  • 接口本身不能创建对象,只能创建接口的实现类对象,接口类型的变量可以与实现类对象构成多态引用。
  • 声明接口用interface,接口的成员声明有限制:(1)公共的静态常量(2)公共的抽象方法(3)公共的默认方法(4)公共的静态方法(5)私有方法(JDK1.9以上)
  • 类可以实现接口,关键字是implements,而且支持多实现。如果实现类不是抽象类,就必须实现接口中所有的抽象方法。如果实现类既要继承父类又要实现父接口,那么继承(extends)在前,实现(implements)在后。
  • 接口可以继承接口,关键字是extends,而且支持多继承。
  • 接口的默认方法可以选择重写或不重写。如果有冲突问题,另行处理。子类重写父接口的默认方法,要去掉default,子接口重写父接口的默认方法,不要去掉default。
  • 接口的静态方法不能被继承,也不能被重写。接口的静态方法只能通过“接口名.静态方法名”进行调用。

6.5.5 经典接口介绍

1、java.lang.Comparable

我们知道基本数据类型的数据(除boolean类型外)需要比较大小的话,之间使用比较运算符即可,但是引用数据类型是不能直接使用比较运算符来比较大小的。那么,如何解决这个问题呢?

Java给所有引用数据类型的大小比较,指定了一个标准接口,就是java.lang.Comparable接口:

package java.lang;

public interface Comparable{
    int compareTo(Object obj);
}

那么我们想要使得我们某个类的对象可以比较大小,怎么做呢?步骤:

第一步:哪个类的对象要比较大小,哪个类就实现java.lang.Comparable接口,并重写方法

  • 方法体就是你要如何比较当前对象和指定的另一个对象的大小

第二步:对象比较大小时,通过对象调用compareTo方法,根据方法的返回值决定谁大谁小。

  • this对象(调用compareTo方法的对象)大于指定对象(传入compareTo()的参数对象)返回正整数
  • this对象(调用compareTo方法的对象)小于指定对象(传入compareTo()的参数对象)返回负整数
  • this对象(调用compareTo方法的对象)等于指定对象(传入compareTo()的参数对象)返回零

代码示例:

package com.atguigu.api;

public class Student implements Comparable {
    private int id;
    private String name;
    private int score;
    private int age;

    public Student(int id, String name, int score, int age) {
        this.id = id;
        this.name = name;
        this.score = score;
        this.age = age;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getScore() {
        return score;
    }

    public void setScore(int score) {
        this.score = score;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", score=" + score +
                ", age=" + age +
                '}';
    }

    @Override
    public int compareTo(Object o) {
        //这些需要强制,将o对象向下转型为Student类型的变量,才能调用Student类中的属性
        //默认按照学号比较大小
        Student stu = (Student) o;
        return this.id - stu.id;
    }
}

测试类

package com.atguigu.api;

public class TestStudent {
    public static void main(String[] args) {
        Student[] arr = new Student[5];
        arr[0] = new Student(3,"张三",90,23);
        arr[1] = new Student(1,"熊大",100,22);
        arr[2] = new Student(5,"王五",75,25);
        arr[3] = new Student(4,"李四",85,24);
        arr[4] = new Student(2,"熊二",85,18);

        //单独比较两个对象
        System.out.println(arr[0].compareTo(arr[1]));
        System.out.println(arr[1].compareTo(arr[2]));
        System.out.println(arr[2].compareTo(arr[2]));

        System.out.println("所有学生:");
        for (int i = 0; i < arr.length; i++) {
            System.out.println(arr[i]);
        }
        System.out.println("按照学号排序:");
        for (int i = 1; i < arr.length; i++) {
            for (int j = 0; j < arr.length-i; j++) {
                if(arr[j].compareTo(arr[j+1])>0){
                    Student temp = arr[j];
                    arr[j] = arr[j+1];
                    arr[j+1] = temp;
                }
            }
        }
        for (int i = 0; i < arr.length; i++) {
            System.out.println(arr[i]);
        }
    }
}

2、java.util.Comparator

思考:

(1)如果一个类,没有实现Comparable接口,而这个类你又不方便修改(例如:一些第三方的类,你只有.class文件,没有源文件),那么这样类的对象也要比较大小怎么办?

(2)如果一个类,实现了Comparable接口,也指定了两个对象的比较大小的规则,但是此时此刻我不想按照它预定义的方法比较大小,但是我又不能随意修改,因为会影响其他地方的使用,怎么办?

JDK在设计类库之初,也考虑到这种情况了,所以又增加了一个java.util.Comparator接口。

package java.util;

public interface Comparator{
    int compare(Object o1,Object o2);
}

那么我们想要比较某个类的两个对象的大小,怎么做呢?步骤:

第一步:编写一个类,我们称之为比较器类型,实现java.util.Comparator接口,并重写方法

  • 方法体就是你要如何指定的两个对象的大小

第二步:比较大小时,通过比较器类型的对象调用compare()方法,将要比较大小的两个对象作为compare方法的实参传入,根据方法的返回值决定谁大谁小。

  • o1对象大于o2返回正整数
  • o1对象小于o2返回负整数
  • o1对象等于o2返回零

代码示例:定义定制比较器类

package com.atguigu.api;

import java.util.Comparator;

public class StudentScoreComparator implements Comparator {
    @Override
    public int compare(Object o1, Object o2) {
        Student s1 = (Student) o1;
        Student s2 = (Student) o2;
        int result = s1.getScore() - s2.getScore();
        return result != 0 ? result : s1.getId() - s2.getId();
    }
}

代码示例:测试类

package com.atguigu.api;

public class TestStudent {
    public static void main(String[] args) {
        Student[] arr = new Student[5];
        arr[0] = new Student(3,"张三",90,23);
        arr[1] = new Student(1,"熊大",100,22);
        arr[2] = new Student(5,"王五",75,25);
        arr[3] = new Student(4,"李四",85,24);
        arr[4] = new Student(2,"熊二",85,18);

        //单独比较两个对象
        System.out.println(arr[0].compareTo(arr[1]));
        System.out.println(arr[1].compareTo(arr[2]));
        System.out.println(arr[2].compareTo(arr[2]));

        System.out.println("所有学生:");
        for (int i = 0; i < arr.length; i++) {
            System.out.println(arr[i]);
        }
        System.out.println("按照学号排序:");
        for (int i = 1; i < arr.length; i++) {
            for (int j = 0; j < arr.length-i; j++) {
                if(arr[j].compareTo(arr[j+1])>0){
                    Student temp = arr[j];
                    arr[j] = arr[j+1];
                    arr[j+1] = temp;
                }
            }
        }
        for (int i = 0; i < arr.length; i++) {
            System.out.println(arr[i]);
        }

        System.out.println("按照成绩排序");
        StudentScoreComparator sc = new StudentScoreComparator();
        for (int i = 1; i < arr.length; i++) {
            for (int j = 0; j < arr.length-i; j++) {
                if(sc.compare(arr[j],arr[j+1])>0){
                    Student temp = arr[j];
                    arr[j] = arr[j+1];
                    arr[j+1] = temp;
                }
            }
        }
        for (int i = 0; i < arr.length; i++) {
            System.out.println(arr[i]);
        }
    }
}

因为会影响其他地方的使用,怎么办?

JDK在设计类库之初,也考虑到这种情况了,所以又增加了一个java.util.Comparator接口。

package java.util;

public interface Comparator{
    int compare(Object o1,Object o2);
}

那么我们想要比较某个类的两个对象的大小,怎么做呢?步骤:

第一步:编写一个类,我们称之为比较器类型,实现java.util.Comparator接口,并重写方法

  • 方法体就是你要如何指定的两个对象的大小

第二步:比较大小时,通过比较器类型的对象调用compare()方法,将要比较大小的两个对象作为compare方法的实参传入,根据方法的返回值决定谁大谁小。

  • o1对象大于o2返回正整数
  • o1对象小于o2返回负整数
  • o1对象等于o2返回零

代码示例:定义定制比较器类

package com.atguigu.api;

import java.util.Comparator;

public class StudentScoreComparator implements Comparator {
    @Override
    public int compare(Object o1, Object o2) {
        Student s1 = (Student) o1;
        Student s2 = (Student) o2;
        int result = s1.getScore() - s2.getScore();
        return result != 0 ? result : s1.getId() - s2.getId();
    }
}

代码示例:测试类

package com.atguigu.api;

public class TestStudent {
    public static void main(String[] args) {
        Student[] arr = new Student[5];
        arr[0] = new Student(3,"张三",90,23);
        arr[1] = new Student(1,"熊大",100,22);
        arr[2] = new Student(5,"王五",75,25);
        arr[3] = new Student(4,"李四",85,24);
        arr[4] = new Student(2,"熊二",85,18);

        //单独比较两个对象
        System.out.println(arr[0].compareTo(arr[1]));
        System.out.println(arr[1].compareTo(arr[2]));
        System.out.println(arr[2].compareTo(arr[2]));

        System.out.println("所有学生:");
        for (int i = 0; i < arr.length; i++) {
            System.out.println(arr[i]);
        }
        System.out.println("按照学号排序:");
        for (int i = 1; i < arr.length; i++) {
            for (int j = 0; j < arr.length-i; j++) {
                if(arr[j].compareTo(arr[j+1])>0){
                    Student temp = arr[j];
                    arr[j] = arr[j+1];
                    arr[j+1] = temp;
                }
            }
        }
        for (int i = 0; i < arr.length; i++) {
            System.out.println(arr[i]);
        }

        System.out.println("按照成绩排序");
        StudentScoreComparator sc = new StudentScoreComparator();
        for (int i = 1; i < arr.length; i++) {
            for (int j = 0; j < arr.length-i; j++) {
                if(sc.compare(arr[j],arr[j+1])>0){
                    Student temp = arr[j];
                    arr[j] = arr[j+1];
                    arr[j+1] = temp;
                }
            }
        }
        for (int i = 0; i < arr.length; i++) {
            System.out.println(arr[i]);
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

水花一直飞

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

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

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

打赏作者

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

抵扣说明:

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

余额充值