【JavaSE】----- 面向对象编程

目录

一、包(package)

🍓导入包中的类

🍓静态导入

🍓将类放到包中

🍓包的访问权限

🍓常见的系统包

二、继承

🍓背景

🍓语法规则

🍓super关键字 

🍓super 与 this 的区别

🍓访问权限总结

🍓更复杂的继承关系

🍓final 关键字

三、组合

四、多态

🍓向上转型

🍓动态绑定

🍓方法重写

🍓向下转型

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

🍓理解多态

🍓使用多态的好处

🍓总结

五、抽象类

🍓语法规则

🍓抽象类内容总结

六、接口

🍓接口内容总结 

🍓对象的比较

🍓深拷贝与浅拷贝代码演示


一、包(package)

  • 是组织类的一种方式.
  • 使用包的主要目的是保证类的唯一性.

🍓导入包中的类

  • Java 中已经提供了很多现成的类供我们使用。

🌊代码示例:

public class TestDemo {
    public static void main(String[] args) {
        int[] arr = {1,2,3,4};
        //打印数组
        System.out.println(java.util.Arrays.toString(arr));

        java.util.Date  date = new java.util.Date();
        //得到一个毫秒级别的时间戳
        System.out.println(date.getTime());
    }
}
  • 可以使用 java.util.Date 这种方式引入 java.util 这个包中的 Date 类与Arrays 类。

🌊上面这种写法比较麻烦一些, 通常使用 import 语句导入包。代码示例:

import java.util.Arrays;
import java.util.Date;

public class TestDemo {
    public static void main(String[] args) {
        int[] arr = {1,2,3,4};
        //打印数组
        System.out.println(Arrays.toString(arr));

        Date  date = new java.util.Date();
        //得到一个毫秒级别的时间戳
        System.out.println(date.getTime());
    }
}
  • 如果需要使用 java.util 中的其他类, 可以使用 import java.util.*; 这里*相当于一个通配符,它可以导入 java.util 包中所有的类。

🌊代码示例:

import java.util.*;
public class TestDemo {
    public static void main(String[] args) {
        int[] arr = {1,2,3,4};
        //打印数组
        System.out.println(Arrays.toString(arr));

        Date  date = new java.util.Date();
        //得到一个毫秒级别的时间戳
        System.out.println(date.getTime());
    }
}
  • 上面的代码中只获取了 java.util 包中的 Arrays 类与 Date 类。但是建议显式的指定要导入的类名. 否则还是容易出现冲突的情况。

解决上面的错误需要使用完整的类名:java.util. Date

import java.util.*;
import java.sql.*;

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

💥注意事项:

  • import 和 C语言中的 #include 差别很大。C语言中,通过 include 关键字导入之后,就会将这个头文件里面的内容全部都拿过来。
  • 通过 import 关键字不能导入具体的包,只能导入这个包底下某个具体的类。Java处理的时候,是需要哪个类就获取哪个类。

🍓静态导入

🍎 使用 import static 可以导入包中的静态的方法和属性

  • 静态导入这种方法使用的很少。虽然这种方式写起来简便,但是代码看着很奇怪,所以不提倡这样写,了解一下即可。

🍓将类放到包中

基本规则

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

⭐操作步骤:

(1)在 IDEA 中先新建一个包: 右键 src ➡ 新建 ➡ 包

(2)在弹出的对话框中输入包名, 例如 com.bit.demo1

(3) 在包中创建类, 右键包名 ➡ 新建 ➡ 类, 然后输入类名即可


(4)此时可以看到我们的磁盘上的目录结构已经被 IDEA 自动创建出来了

说明

  • 在一个包中写一个类,其实就是在一个文件夹中写一个java文件。 同一个文件夹中不可以创建两个名字相同的文件,所以在同一个包中不可以创建两个名字相同的类。包的主要目的是保证类的唯一性。
  • 例如, 在同一个包中写两个 Test 类,就会冲突, 导致代码不能编译通过。
  • 如果要创建了两个相同名称的类,应放在不同的包中。

(5)在包中新创建的 Test.java 文件的最上方, 出现了一个 package 语句

🍎package 这条语句:指定当前 Test 类在 com.bit.demo1 这个包中。

🎄package 与 import

  • package:“包”,指类所在的包
  • import:“引入”,指引入需要使用的类

🍓包的访问权限

  • 前面已经介绍了类中的 public 和 private。 private 中的成员只能被类的内部使用。
  • 包访问权限:只能在当前包中使用,当成员变量不加任何访问修饰限定词时,默认就是包访问权限。此时这个成员可以在包内部的其他类使用, 但是不能在包外部的类使用。

🌊代码示例:

//在 Test 类中创建了一个变量
package com.bit.demo1;

public class Test {
    int val = 10; // 默认是包访问权限
}

//在与 Test 类的同一个包下的 TestDemo 类中创建 Test 对象,并使用 Test 类中的成员变量
package com.bit.demo1;

public class TestDemo {
    public static void main(String[] args) {
        Test test = new Test();
        System.out.println(test.val);
    }
}

运行结果

不同包中的类不可以访问。

🍓常见的系统包

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

二、继承

🍓背景

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

🌊代码示例:

class Dog{
    public String name;
    public int age;

    public void eat(){
        System.out.println("eat()");
    }
}

class Bird{
    public String name;
    public int age;
    public String wing;

    public void eat(){
        System.out.println("eat()");
    }

    public void fly(){
        System.out.println("fly()");
    }
}

这个代码中存在了大量的冗余,有一些相同的属性和方法。可以将这些相同的属性和方法抽取出来放到一个类中,Dog与Bird都是动物,可以写一个 Animal 类存放相同的属性和方法。

  • 这两个类都具备一个相同的 eat 方法, 而且行为是完全一样的.
  • 这两个类都具备一个相同的 name 和 age 属性, 而且意义是完全一样的.
  • 从逻辑上讲, Dog 和 Bird 都是一种 Animal (is - a 语义).

可以让 Dog 和 Bird 分别继承 Animal 类, 来达到代码重用的效果.

  • Animal 这样被继承的类, 我们称为 父类 , 基类 或 超类,
  • Dog 和 Bird 这样的类, 我们称为 子类, 派生类
  • 这样的形式和现实中的儿子继承父亲的财产类似, 子类也会继承父类的字段和方法, 以达到代码重用的效果。

继承:对共性的抽取,使用 extends 关键字进行处理。意义在于:可以对代码进行重复使用。

🍓语法规则

基本语法

class 子类 extends 父类 {

}
  • 使用 extends 指定父类.
  • Java 中一个子类只能继承一个父类.
  • 子类会继承父类的所有 public 的字段和方法.
  • 对于父类的 private 的字段和方法, 子类中是无法访问的.
  • 子类的实例中, 也包含着父类的实例. 可以使用 super 关键字得到父类实例的引用。
  • extends 英文原意指 "扩展".。所写的类的继承, 也可以理解成基于父类进行代码上的 "扩展"。

对于上面的代码, 可以使用继承进行改进. 让 Dog 和 Bird 继承自 Animal 类。

🌊代码示例:

class Animal{
    public String name;
    public int age;

    public void eat(){
        System.out.println(this.name + "正在吃");
    }
}

class Dog extends Animal{

}

class Bird extends Animal{
    public String wing;

    public void fly(){
        System.out.println(this.name+"正在飞");
    }
}

public class TestDemo {
    public static void main(String[] args) {
        Dog dog = new Dog();
        dog.name = "小黑";
        dog.eat();
        Bird bird = new Bird();
        bird.name = "大白";
        bird.eat();
        bird.fly();
    }
}

运行结果

 如果把 name 改成 private, 那么此时子类就不能访问了。

🍓super关键字 

✨说明:

  • 子类继承了父类,子类构造的时候,需要要先帮助父类进行构造(需要在子类的构造方法中使用super关键字来显示的调用父类的构造方法

🌊代码示例:

class Animal{
    public String name;
    public int age;

    public Animal(String name,int age){
        this.name = name;
        this.age =age;
    }
    public void eat(){
        System.out.println(this.name + "正在吃");
    }
}

class Dog extends Animal{
    public Dog(String name,int age){
        //调用父类带有两个参数的构造方法
        super(name,age); //显示调用父类的构造方法
    }
}
  • super :代表父类对象的引用,所以不能出现在静态的构造方法中。
  • super(); :调用父类的构造方法,必须放在构造方法中的第一行。(只能在构造方法中使用)
  • super.func();:调用父类的普通方法。(可以在普通方法中使用)
  • super.data;:调用父类的成员变量。(可以在普通方法中使用)

🌊代码示例:

class Animal{
    public String name;
    public int age;

    public Animal(String name,int age){
        this.name = name;
        this.age =age;
    }
    public void eat(){
        System.out.println(this.name + "正在吃");
    }
}

class Dog extends Animal{
    public Dog(String name,int age){
        super(name,age); //调用父类带有两个参数的构造方法
    }
}

class Bird extends Animal{
    public String wing;

    public Bird(String name,int age){
        super(name,age); //调用父类带有两个参数的构造方法
    }
    public void fly(){
        System.out.println(this.name+"正在飞");
    }
}

public class TestDemo {
    public static void main(String[] args) {
        Dog dog = new Dog("小黑",15);
        dog.eat();
        Bird bird = new Bird( "大白",5);
        bird.eat();
        bird.fly();
    }
}

内存布局

运行结果

前面的代码中没有写构造方法时代码也可以执行,是因为一个类没有提供任何构造方法时,默认会有一个无参的构造方法。如果一个类中提供了构造方法,就不会有默认的构造方法。

//Dog 类中默认的构造方法   

 public Dog(){
        super(); 
    }

如果子类中的成员变量与父类的成员变量同名,在调用时没有使用super关键字,就优先访问子类的成员变量。

🌊代码示例:

class Animal{
    public String name;
    public int age;

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

class Bird extends Animal{

    public String wing;
    public String name;

    public Bird(String name,int age){
        super(name,age); //调用父类带有两个参数的构造方法
    }
    public void fly(){
        System.out.println(this.name+"正在飞");
    }
}

public class TestDemo {
    public static void main(String[] args) {
        Bird bird = new Bird( "大白",5);
        System.out.println(bird.name);
        bird.fly();
    }
}

运行结果

🍓super 与 this 的区别

🍓访问权限总结

protected 关键字

  • 如果把字段设为 private, 子类不能访问. 但是设成 public, 又违背了我们 "封装" 的初衷.
  • 两全其美的办法就是 protected 关键字.
  • 对于类的调用者来说, protected 修饰的字段和方法是不能访问的
  • 对于类的子类同一个包的其他类 来说, protected 修饰的字段和方法是可以访问的

  • 同一个类中无论使用什么关键字修饰都可以访问,所以就不演示了。
  • default 包访问权限,前面已经演示了。
  • public 修饰的成员变量或方法,在哪里使用都可以,所以就不演示了。

protected 修饰的成员变量或方法 ➡ 被同一个包中的其他类访问

protected 修饰的成员变量或方法 ➡ 被不同包中的子类访问(继承的类也是public修饰的)

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

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

什么情况下用哪一种关键字修饰?

  • 我们希望类要尽量做到 "封装", 即隐藏内部实现细节, 只暴露出必要的信息给类的调用者.
  • 因此在使用的时候应该尽可能的使用比较严格的访问权限. 例如:如果一个方法能用 private, 就尽量不要用public.
  • 另外, 还有一种 简单粗暴 的做法: 将所有的字段设为 private, 将所有的方法设为 public. 不过这种方式属于是对访问权限的滥用, 写代码的时候需要认真思考, 该类提供的字段方法到底给 "谁" 使用(是类内部自己用, 还是类的调用者使用, 还是子类使用)

🍓更复杂的继承关系

  • 刚才的例子中, 只涉及到 Animal, Dog 和 Bird 三种类. 但是如果情况更复杂一些呢?
  • 例如 Cat 的情况, 可能需要表示更多种类的猫。

如果使用继承方式来表示, 就会涉及到更复杂的体系。

🌊代码示例:

// Animal.java
public Animal {
...
}
// Cat.java
public Cat extends Animal {
...
}
// ChineseGardenCat.java
public ChineseGardenCat extends Cat {
...
}
// OrangeCat.java
public Orange extends ChineseGardenCat {
...
}
......

这样的继承方式称为多层继承, 即子类还可以进一步的再派生出新的子类.

  • 一般不希望出现超过三层的继承关系. 如果继承层次太多, 就需要考虑对代码进行重构。
  • 如果想从语法上进行限制继承, 就可以使用 final 关键字。

🍓final 关键字

  • final 修饰变量或者字段的时候, 不可以被修改(表示常量)
  • final 修饰类, 表示被修饰的类不可以被继承
  • final 修饰方法

final 关键字的功能是限制类被继承,"限制" 意味着 "不灵活". 在编程中, 灵活往往不见得是一件好事. 灵活可能意味着更容易出错。

💦平时是用的 String 字符串类, 就是用 final 修饰的, 不能被继承。

三、组合

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

🌊代码示例:表示一个学校

public class Student {
...
}
public class Teacher {
...
}
public class School {
    public Student[] students;
    public Teacher[] teachers;
}
  • 组合并没有涉及到特殊的语法(诸如 extends 这样的关键字), 仅仅是将一个类的实例作为另外一个类的字段.

组合表示 has - a 语义

  • 可以理解成一个学校中 "包含" 若干学生和教师

继承表示 is - a 语义

  •  "动物和狗" 的例子中, 我们可以理解成一只狗也 "是" 一种动物

四、多态

🍓向上转型

父类 (Animal) 的引用, 指向一个子类 (Dog) 的对象,这种写法称为 向上转型。

向上转型发生的时机:

  • 直接赋值
  • 方法传参
  • 方法返回

(1)直接赋值

class Animal{
    public String name;
    public int age;

    public Animal(String name,int age){
        this.name = name;
        this.age =age;
    }
    public void eat(){
        System.out.println(this.name + "正在吃");
    }
}

class Dog extends Animal {
    public Dog(String name,int age){
        super(name,age); //调用父类带有两个参数的构造方法
    }
}
public class TestDemo {
    public static void main(String[] args) {
      /*  Dog dog = new Dog("小黑",15);
        Animal animal = dog; // 父类引用  引用 子类对象*/

        //向上转型
        Animal animal = new Dog("小黑",15); // 父类引用  引用 子类对象
        animal.eat();
    }
}

 运行结果

(2)方法传参

class Animal{
    public String name;
    public int age;

    public Animal(String name,int age){
        this.name = name;
        this.age =age;
    }
    public void eat(){
        System.out.println(this.name + "正在吃");
    }
}

class Dog extends Animal {
    public Dog(String name,int age){
        super(name,age); //调用父类带有两个参数的构造方法
    }
}
public class TestDemo {
    public static void func(Animal animal){
        System.out.println(animal.name+"是"+animal.age+"岁");
    }
    public static void main(String[] args) {
        Dog dog = new Dog("小黑",15);
        func(dog);
    }
}

运行结果

 (3)方法返回

package com.bit.demo3;

class Animal{
    public String name;
    public int age;

    public Animal(String name,int age){
        this.name = name;
        this.age =age;
    }
    public void eat(){
        System.out.println(this.name + "正在吃");
    }
}

class Dog extends Animal {
    public Dog(String name,int age){
        super(name,age); //调用父类带有两个参数的构造方法
    }
}
public class TestDemo {
    public static Animal func(){
        Dog dog = new Dog("小黑",19);
        return dog;
    }

    public static void main(String[] args) {
        Animal animal = func();
        System.out.println(animal);
    }
}

运行结果

🍓动态绑定

  • 当子类与父类出现同名的方法时,会调用哪个方法?

🌊代码示例:

class Animal{
    public String name;
    public int age;

    public Animal(String name,int age){
        this.name = name;
        this.age =age;
    }
    //父类的 eat 方法
    public void eat(){
        System.out.println(this.name + "正在吃");
    }
}

class Dog extends Animal {
    public Dog(String name,int age){
        super(name,age); //调用父类带有两个参数的构造方法
    }
    //子类的 eat 方法
    @Override
    public void eat(){
        System.out.println(name + "正在吃骨头");
    }
}
public class TestDemo {
    public static void main(String[] args) {
        Animal animal1 = new Animal("大白",20);
        animal1.eat();

        //向上转型
        Animal animal2 = new Dog("小黑",15); // 父类引用  引用 子类对象
        animal2.eat();
    }
}

 运行结果

  • animal1 和 animal2 虽然都是 Animal 类型的引用, 但是 animal1 指向 Animal 类型的对象, animal2 指向 Dog 类型的对象.
  • animal1 和 animal2 分别调用 eat 方法,  animal1.eat() 实际调用了父类的方法, 而 animal2.eat() 调用了子类的方法。

在 Java 中, 调用某个类的方法, 究竟执行了哪段代码 (是父类方法的代码还是子类方法的代码) , 要看引用指向的是父类对象还是子类对象。

代码在编译的时候不能确定调用的是谁的方法,只有在运行的时候才知道调用的是哪个类的方法。这就是 动态绑定 也叫 运行时绑定

🌀动态绑定的前提:

  1. 父类引用 引用 子类的对象
  2. 通过这个父类引用,调用父类和子类同名的覆盖方法(重写)

补充:静态绑定(编译时绑定)

  • 编译时绑定:通过方法的重载实现。在编译的时候,会根据参数的个数和类型,在编译期间确定最终调用哪一个方法。

通过父类引用,只能访问父类自己的成员 

🌊代码示例:

class Animal{
    public String name;
    public int age;

    public Animal(String name,int age){
        this.name = name;
        this.age =age;
    }
    public void eat(){
        System.out.println(this.name + "正在吃");
    }
}

class Dog extends Animal {
    public String name = "嘿嘿";
    public Dog(String name,int age){
        super(name,age); //调用父类带有两个参数的构造方法
    }
}
public class TestDemo {
    public static void main(String[] args) {
        Animal animal = new Animal("大白",10);
        System.out.println(animal.name);
    }
}

运行结果

🍓方法重写

  • 方法名相同
  • 参数列表相同(个数、类型)
  • 返回值相同【   特殊:返回值可以是协变类型(返回值构成父子类关系) 】
  • 重写必须是在父子类的情况下

🍎子类实现父类的同名方法, 并且参数的类型和个数完全相同, 这种情况称为 覆写/重写/覆盖(Override)
(1)重写的注意事项

  1. 普通方法可以重写, static 修饰的静态方法不能重写
  2. private修饰的方法不能重写
  3. 被 final 修饰的方法不能重写
  4. 重写中子类方法的访问权限不能低于父类方法访问权限.
  5. 重写的方法返回值类型不一定和父类的方法相同【但是建议最好写成相同, 特殊情况(协变类型)除外】

✨协变类型(方法重写,返回值不同)

(2)重写的方法, 可以使用 @Override 注解来显式指定 

  • 这个注解能帮我们进行一些合法性校验. 例如不小心将方法名字拼写错了 (比如写成 aet), 那么此时编译器就会发现父类中没有 aet 方法, 就会编译报错, 提示无法构成重写.
  • 推荐在代码中进行重写方法时显式加上 @Override 注解

(3)重写与重载的区别:

  • 重载:在同一个类中,当方法名相同,形参列表不同的时候,多个方法构成了重载。
  • 重写:在不同的类中,子类对父类提供的方法不满意的时候,要对父类的方法进行重写。

🍓向下转型

  • 向上转型是子类对象转成父类对象, 向下转型就是父类对象转成子类对象.
  • 相比于向上转型来说, 向下转型没那么常见,但是也有一定的用途。

💥注意事项:

  • 编译过程中, animal 的类型是 Animal, 此时编译器只知道这个类中有一个 eat 方法, 没有 fly 方法.
  • 虽然 animal 实际引用的是一个 Bird 对象, 但是编译器是以 animal 的类型来查看有哪些方法的.

对于  Animal animal = new Bird("小白",6);这样的代码:

  • 编译器检查有哪些方法存在, 看的是 Animal 这个类型
  • 执行时究竟执行父类的方法还是子类的方法, 看的是 Bird 这个类型.
  • 如果要调用 fly() 方法, 就需要向下转型

🌊代码示例:

class Animal{
    public String name;
    public int age;

    public Animal(String name,int age){
        this.name = name;
        this.age =age;
    }
    public void eat(){
        System.out.println(this.name + "正在吃");
    }
}

class Bird extends Animal {

    public String wing;
    public Bird(String name,int age){
        super(name,age); //调用父类带有两个参数的构造方法
    }
    public void fly(){
        System.out.println(this.name+"正在飞");
    }
}
public class TestDemo {
    public static void main(String[] args) {
        Animal animal = new Bird("小鸟",6);
        //向下转型
        Bird bird = (Bird)animal;  //需要强制类型转换
        bird.fly();
    }

运行结果

向下转型有时是不靠谱的

  • animal 本质上引用的是一个 Dog 对象, 是不能转成 Bird 对象的. 运行时就会抛出异常.
  • 所以, 为了让向下转型更安全, 需要先判定一下看 animal 本质上是不是一个 Bird 实例, 再来转换

🌊代码示例:

public class TestDemo {
    public static void main(String[] args) {
        Animal animal = new Bird("小鸟",6);
        //向下转型
        if(animal instanceof Bird) {
            Bird bird = (Bird)animal;  //需要强制类型转换
            bird.fly();
        }
    }
}

instanceof 可以判定一个引用是否是某个类的实例. 如果是, 则返回 true. 这时再进行向下转型就比较安全了。

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

🌊代码示例:

class Animal{
    public String name = "hello";
    public int age;

    public Animal(String name,int age){
        eat();
        this.name = name;
        this.age =age;
    }
    public void eat(){
        System.out.println(name + "正在吃");
    }
}

class Dog extends Animal {
    public Dog(String name,int age){
        super(name,age); //调用父类带有两个参数的构造方法
    }

    @Override
    public void eat(){
        System.out.println(name+"在吃骨头");
    }
}

public class Test {
    public static void main(String[] args) {
        Dog dog = new Dog("小狗",20);
    }
}

运行结果

🍎构造方法中调用子类重写的方法也会发生动态绑定。

结论: 尽量不要在构造器中调用方法(如果这个方法被子类重写, 就会触发动态绑定, 但是此时子类对象还没构造完成), 可能会出现一些隐藏的但是又极难发现的问题。

🍓理解多态

  • 多态的前提是向上转型。
  • 通过一个引用调用同一个方法(子类重写父类的方法),因为引用的对象不同,所以调用方法产生的结果也不同,这种表象就是多态。
  • 多态就是 "一个引用, 能表现出多种不同形态"

🌊代码示例:

class Shape{
    public void draw(){

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

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

/***************分割线******************/

public class TestDemo {
    //打印单个图形
    public static void drawMap(Shape shape){
        shape.draw();
    }
    public static void main(String[] args) {
        Rect rect = new Rect();
        drawMap(rect);
        Flower flower = new Flower();
        drawMap(flower);
    }
}
  • 在这个代码中, 分割线上方的代码是由 类的实现者 编写的, 分割线下方的代码是由 类的调用者 编写的。
  • 当类的调用者在编写 drawMap 这个方法时, 参数类型为 Shape (父类), 此时在该方法内部并不知道, 也不关注当前的 shape 引用指向的是哪个类型(哪个子类)的对象。
  • 此时 shape 这个引用调用 draw 方法可能会有多种不同的表现(和 shape 对应的对象相关), 这种行为就称为 多态

🍓使用多态的好处

(1)类调用者对类的使用成本进一步降低.

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

因此, 多态可以理解成是封装的更进一步, 让类调用者对类的使用成本进一步降低

(2)可扩展能力更强

  • 如果要新增一种新的形状, 使用多态的方式代码改动成本也比较低
 class Triangle extends Shape{
        @Override
        public void draw() {
            System.out.println("△");
        }
    }
  • 对于类的调用者来说(drawMap方法), 只要创建一个新类的实例就可以了, 改动成本很低.
  • 而对于不用多态的情况, 就要把 drawShapes 中的 if - else 进行一定的修改, 改动成本更高

(3)能够降低代码的 "圈复杂度", 避免使用大量的 if - else

例如:需要打印的不是一个形状了, 而是多个形状. 如果不基于多态, 实现代码如下

public class TestDemo {
    public static void drawMap(){
        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 main(String[] args) {
        drawMap();
    }
}

如果使用使用多态, 则不必写这么多的 if - else 分支语句, 代码更简单

public class TestDemo {
    public static void drawMap(){
        Rect rect = new Rect();
        Flower flower = new Flower();
        Triangle triangle = new Triangle();

        Shape[ ] shapes = {triangle, rect, triangle, rect, flower};
        for (Shape shape : shapes) {
            shape.draw();
        }
    }
    public static void main(String[] args) {
        drawMap();
    }
}

🌌什么叫 "圈复杂度" ?

  • 圈复杂度是一种描述一段代码复杂程度的方式. 一段代码如果平铺直叙, 那么就比较简单容易理解. 而如果有很多的条件分支或者循环语句, 就认为理解起来更复杂.
  • 因此可以简单粗暴的计算一段代码中条件语句和循环语句出现的个数, 这个个数就称为 "圈复杂度". 如果一个方法的圈复杂度太高, 就需要考虑重构.

🍓总结

 如果抛开 Java, 多态其实是一个更广泛的概念, 和 "继承" 这样的语法并没有必然的联系.
  • C++ 中的 "动态多态" 和 Java 的多态类似. 但是 C++ 还有一种 "静态多态"(模板), 就和继承体系没有关系了.
  • Python 中的多态体现的是 "鸭子类型", 也和继承体系没有关系.
  • Go 语言中没有 "继承" 这样的概念, 同样也能表示多态.

无论是哪种编程语言, 多态的核心都是让调用者不必关注对象的具体类型. 这是降低用户使用成本的一种重要方式。

五、抽象类

🍓语法规则

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

🍓抽象类内容总结

1、什么是抽象方法?

  • 一个没有具体实现的方法,被abstract修饰。

2、包含抽象方法的类叫做抽象类

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

3、抽象类不可以被实例化(new)

4、 抽象类不能被实例化,只能被继承。

5、抽象类中可以包含其他的非抽象方法, 也可以包含成员变量。

  • 非抽象方法和普通方法的规则都是一样的, 可以被重写,也可以被子类直接调用

6、一个普通类继承了一个抽象类,那么这个普通类中,需要重写抽象类中所有抽象的方法。

🌊代码示例:

abstract  class Shape{
    int val = 10;
    public abstract void draw();

    public void func(){
        System.out.println("func()方法");
    }
}

class Rect extends Shape {
    @Override
    public void draw() {
        System.out.println("♦");

        //调用抽象类中的普通方法
        super.func();
    }
}

class Flower extends Shape {
    @Override
    public void draw() {
        System.out.println("❀");

        //调用抽象类中的成员变量
        System.out.println(val);
    }
}

public class Test {
    public static void drawMap(Shape shape){
        shape.draw();
    }
    public static void main(String[] args) {
        //向上转型
        Shape shape = new Rect() ;
        drawMap(shape);

        Flower flower = new Flower();
        drawMap(flower);
    }
}

运行结果

7、一个抽象类 B 如果继承了一个抽象类 A,那么抽象 B 可以不实现抽象父类 A 的抽象方法。

🌊代码示例: 

abstract class A{
    public abstract void funcA();
}
abstract class B extends A{
    public abstract void funcB();
}

8、结合第8点,当抽象类 B 再次被一个普通类 C 继承后,那么普通类 C 中必须重写 A 和 B 这两个抽象类当中的抽象方法。

🌊代码示例:

abstract class A{
    public abstract void funcA();
}

abstract class B extends A{
    public abstract void funcB();
}

class C extends B{
    @Override
    public void funcA() {
        
    }
    @Override
    public void funcB() {
        
    }
}

9、抽象类不可以被 final 修饰。

10、抽象方法不可以被 final 和 private 修饰。

11、抽象类的作用:

  • 抽象类存在的最大意义就是为了被继承.
  • 抽象类本身不能被实例化, 要想使用, 只能创建该抽象类的子类. 然后让子类重写抽象类中的抽象方法.

普通的类也可以被继承, 普通的方法也可以被重写, 为什么非要用抽象类和抽象方法呢?

  • 使用抽象类相当于多了一重编译器的校验。

六、接口

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

语法规则

  • 在刚才的代码中, 我父类 Shape 并没有包含别的非抽象方法, 也可以设计成一个接口

🍓接口内容总结 

1、接口使用 interface 修饰的。 interface IA {}

interface IShape{
    public abstract void draw();
}

2、接口当中的普通方法不能具体的实现,如果非要实现,只能通过关键字 default 修饰这个方法。

interface IShape{
    public abstract void draw();
    
    default public void func(){
        System.out.println("default 修饰的func()方法");
    }
}

3、接口中可以有 static 修饰的方法

interface IShape{
    public abstract void draw();

    public static void func(){
        System.out.println("static 修饰的func()方法");
    }
}

4、接口中的方法一定是 public, 因此可以省略 public

5、接口中的抽象方法默认是 public abstract 的, 因此可以省略 public abstract

interface IShape{
    //抽象方法
    void draw();

     static void func(){
        System.out.println("static 修饰的func()方法");
    }
}

6、接口不可以被实例化(new)。

7、类和接口之间的关系是通过 implements 实现的。

8、当一个类实现了一个接口,就必须重写接口当中的抽象方法。

9、当一个类实现一个接口之后,重写这个方法的时候,方法前面必须加上public。

10、接口在调用的时候可以创建一个接口的引用, 对应到一个子类的实例。

🌊代码示例:

interface IShape{
    public abstract void draw();

    default public  void func(){
        System.out.println("default 修饰的func()方法");
    }
}

class Rect implements IShape {
    @Override
    public void draw() {
        System.out.println("♦");
    }

    @Override
    public void func() {
        System.out.println("重写接口中的func()方法");
    }
}

class Flower implements IShape {
    @Override
    public void draw() {
        System.out.println("❀");
    }
}
public class Test {
    public static void drawMap(IShape iShape){
        iShape.draw();
    }
    public static void main(String[] args) {
        //向上转型
        IShape iShape = new Rect();
        drawMap(iShape);
        //调用子类 Rect 中重写的func方法
        iShape.func();
        
        Flower flower = new Flower();
        drawMap(flower);
    }
}

运行结果

 11、接口中的成员变量默认是public static final 修饰的

interface IShape{
    int a = 10; //因为默认是 public static final 修饰的(常量)必须要初始化
    
    void draw(); //抽象方法
}

12、 一个类可以通过关键字 extends 继承一个抽象类或者普通类,但是只能继承一个类。同时,可以通过 implements 实现对多个接口,接口之间使用逗号隔开。

🌊代码示例:

interface IA{
    int A = 10;
    void funcA();
}
interface IB{
    void funcB();
}
abstract class BClass{
    public abstract void funcBClass();
}
class AClass extends BClass implements IA,IB{
    //重写接口IA中的 funcA() 方法
    @Override
    public void funcA() {
        System.out.println("重写接口中的funcA()方法");

        System.out.println("调用接口中的成员变量A:"+A);
    }
    //重写接口IB中的 funcB() 方法
    @Override
    public void funcB() {
        System.out.println("重写接口中的funcB()方法");
    }
    //重写抽象类BClass中的 funcBClass() 方法
    @Override
    public void funcBClass(){
        System.out.println("重写抽象类中的funcBClass()方法");
    }
}

13、接口和接口之间可以使用 extends 来操作他们的关系,此时 extends 意为:拓展。

  • 一个接口 IB 通过 extends 拓展另一个接口 IA 的功能。此时当一个类 C 通过 implements 实现这个接口 IB 的时候,不仅要重写接口 IB 中的方法,还用重写他从 IA 接口拓展来的功能【方法】。

🌊代码示例: 

interface IA{
    void funcA();
}
interface IB extends IA{
    void funcB();
}
class C implements IB{
    @Override
    public void funcB() {

    }

    @Override
    public void funcA() {

    }
}

14、实现多个接口

  • 有的时候需要让一个类同时继承自多个父类. 这件事情在有些编程语言通过 多继承 的方式来实现的.
  •  Java 中只支持单继承, 一个类只能 extends 一个父类. 但是可以同时实现多个接口, 也能达到多继承类似的效果。

🌊代码示例: 

class Animal{
    public String name;

    public Animal(String name) {
        this.name = name;
    }
}
//不是所有的动物都会飞,所以不能写到Animal类中。
//写到另一个类中也不行,因为一个类不能继承多个类,所以使用接口实现。
interface IFlying{
    void fly();
}
//跑的接口
interface IRunning{
    void run();
}
//游泳的接口
interface ISwimming{
    void swim();
}
//鸟会飞
class Bird extends Animal implements IFlying{
    public Bird(String name) {
        super(name);
    }

    @Override
    public void fly() {
        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,IFlying,ISwimming{
    public Duck(String name){
        super(name);
    }

    @Override
    public void run() {
        System.out.println(this.name+"正在跑");
    }

    @Override
    public void fly() {
        System.out.println(this.name+"正在飞");
    }

    @Override
    public void swim() {
        System.out.println(this.name+"正在游泳");
    }
}
public class Test1 {
    //调用飞的方法
    public static void Flyingfunc(IFlying iFlying){
        iFlying.fly();
    }
    //调用跑的方法
    public static void Runningfunc(IRunning iRunning){
        iRunning.run();
    }
    //调用游泳的方法
    public static void Swimmingfunc(ISwimming iSwimming){
        iSwimming.swim();
    }
    public static void main(String[] args) {
        /*Bird bird = new Bird("小鸟");
        Flyingfunc(bird);*/
        Flyingfunc(new Bird("小鸟"));

        Flyingfunc(new Duck("鸭子"));
        Runningfunc(new Duck("鸭子"));
        Swimmingfunc(new Duck("鸭子"));

        Runningfunc(new Frog("青蛙"));
        Swimmingfunc(new Duck("青蛙"));
    }
}

运行结果

上面的代码展示了 Java 面向对象编程中最常见的用法:

  • 一个类继承一个父类, 同时实现多种接口.
  • 继承表达的含义是 is - a 语义, 而接口表达的含义是 具有 xxx 特性 。

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

15、三个常用接口

🎄Comparable 接口

🌊代码示例: 给定一个学生类

class Student{
    public String name;
    public int age;
    public double score;

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

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

🌊代码示例: 再给定一个学生对象数组

public class Test {
    public static void main(String[] args) {
        Student[] students = new Student[3];
        students[0] = new Student("zhangsan",20,89.6);
        students[1] = new Student("lisi",22,77.6);
        students[2] = new Student("wangwu",19,66.6);
        System.out.println(Arrays.toString(students));
    }
}

对这个对象数组中的元素进行排序。

数组有一个现成的 sort 方法, 能否直接使用这个方法呢?

学生对象和普通的整数不一样, 两个整数是可以直接比较的, 大小关系明确. 而两个学生对象的大小关系需要额外指定.

可以让 Student 类实现 Comparable 接口, 在Student类中重写 compareTo 方法

🌊代码示例:  

//使用comparable接口,比较学生类
class Student implements Comparable<Student>{
    public String name;
    public int age;
    public double score;

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

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

    //额外指定数组比较的内容
    @Override
    public int compareTo(Student o) {
        //按年龄比较
        //谁调用compareTo方法,谁就是this

       /* if(this.age>o.age){
            return 1;
        }else if(this.age<o.age){
            return -1;
        }else {
            return 0;
        }*/

        //代码简化
        return this.age - o.age;     //从小到大排序

       // return o.age - this.age;  //从大到小排序
    }
}    

在 sort 方法中会自动调用重写后的 compareTo 方法。compareTo 的参数是 Object , 其实传入的就是 Student 类型的对象。然后比较当前对象和参数对象的大小关系(按年龄来算)。

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

//按分数比较
//成绩是double类型的,所以要进行强制类型转换
return (int)(this.score - o.score);

//按姓名比较
//要是用 compareTo() 方法进行比较
return this.name.compareTo(o.name);
  • 如果是自定义类型进行比较,一定要实现可以比较的接口。
  • Comparable 这个接口有一个很大的缺点:对类的侵入性非常强。代码一旦写好了,不能轻易改动。

🎄Comparator 接口

🌊代码示例: 给定一个学生类

class Student {
    public String name;
    public int age;
    public double score;

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

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

🌊代码示例: 再给定一个学生对象数组

public class Test {
    public static void main(String[] args) {
        Student[] students = new Student[3];
        students[0] = new Student("zhangsan",20,89.6);
        students[1] = new Student("lisi",22,77.6);
        students[2] = new Student("wangwu",19,66.6);
        System.out.println(Arrays.toString(students));
    }
}

🌊代码示例: 写一个 AgeComparator 类实现 Comparator 接口。按年龄进行比较

//按年龄进行比较
class AgeComparator implements Comparator<Student>{
    @Override
    public int compare(Student o1, Student o2) {
        return o1.age - o2.age;
    }
}

🌊代码示例: 对 AgeComparator 类进行实例化,在sort方法中调用 AgeComparator 对象,对数组进行比较。

public class Test {
    public static void main(String[] args) {
        Student[] students = new Student[3];
        students[0] = new Student("zhangsan",20,89.6);
        students[1] = new Student("lisi",22,77.6);
        students[2] = new Student("wangwu",19,66.6);
        System.out.println("比较前:");
        System.out.println(Arrays.toString(students));

        //实例化 AgeComparator
        AgeComparator ageComparator = new AgeComparator();

        System.out.println("比较后:");

        //sort方法中,传入学生数组,传入对学生数组进行比较的对象的引用(按年龄比较)
        Arrays.sort(students,ageComparator);

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

运行结果

🌊代码示例:  

//按分数进行比较
class ScoreComparator implements Comparator<Student>{
    @Override
    public int compare(Student o1, Student o2) {
        return (int)(o1.score - o2.score);
    }
}

ScoreComparator scoreComparator = new ScoreComparator();
Arrays.sort(students,scoreComparator);


//按姓名进行比较
class NameComparator implements Comparator<Student>{
    @Override
    public int compare(Student o1, Student o2) {
        return o1.name.compareTo(o2.name);
    }
}

NameComparator nameComparator = new NameComparator();
Arrays.sort(students,nameComparator);
  • Comparator 很灵活,对类的侵入性非常弱。
  • Comparator 是java.util 包中的泛型接口类,使用时必须导入对应的包。

🎄Clonable 接口

创建对象的方式:

  1. new
  2. 使用 clone() 方法

 一个对象要克隆产生一个副本,这个对象一定要是可以克隆的对象,所以要对这个对象实现Clonable 接口。

一个类实现一个接口首先要重写接口中的方法,但是可以看到 Clonable 接口中什么都没有。

这里会有一个问题:Clonable 这个空接口有什么作用?

  • 如果没有实现Cloneable接口,在使用clone()方法时会抛出 CloneNotSupportedException 异常,所以可以把Cloneable接口看成是实现clone()方法必要的一个因素,把这个接口称为标志接口。
  • 标志接口的作用:代表当前这个类是可以被克隆的。

💦clone()方法:

可以看到Java中的clone()方法返回的是一个 Object 类型,并且抛出了一个异常(异常的内容会在后面的文章中介绍), 所以在使用 clone() 方法时要对其进行强制类型转换,并且抛出一个异常。

clone()方法比较特殊:如果要调用clone()方法,就必须重写clone()方法

🌊代码示例:   

class Person implements Cloneable{
    public int age;
    public void eat(){
        System.out.println("吃!");
    }

    @Override
    public String toString() {
        return "Person{" +
                "age=" + age +
                '}';
    }

    //重写 clone() 方法
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

public class TestDemo1  {
    public static void main(String[] args) throws CloneNotSupportedException {
        Person person1 = new Person();
        person1.age = 99;
        //clone() 默认是 Object 类,所以将其进行强制类型转换
        Person person2 = (Person) person1.clone();
        System.out.println(person2.age);
        System.out.println("====================");
        person2.age=50;
        System.out.println(person1.age);
        System.out.println(person2.age);
    }

}

内存布局

运行结果

一个类生成的对象要想被克隆,必须要实现Cloneable接口。实现Cloneable接口后,要重写 clone()方法,同时要抛出异常。

🍓对象的比较

  • 前面已经介绍了两种对象的比较方:实现 Comparable 接口或Comparator 接口。还有一种比较方式:重写基类的 equal 方法
  • 对于实现的自定义类型,都默认继承自Object类。而Object类中提供了equal方法,两个引用直接使用==进行比较,默认情况下调用的就是equal方法。
  • equal 方法的比较规则是:没有比较引用变量指向对象的内容,而是直接比较引用变量的地址,有些情况下该种比较就不符合题意,所以需要重写equal 方法进行比较。

🌊代码示例:   

class Card {
    public int rank; // 数值
    public String suit; // 花色

    public Card(int rank, String suit) {
        this.rank = rank;
        this.suit = suit;
    }

    @Override
    public boolean equals(Object o) {
        // 如果this引用与o引用指向的是同一个对象,就返回true
        if (this == o) {
            return true;
        }
        // o如果是null对象,或者o不是Card的子类
        if (o == null || !(o instanceof Card)) {
            return false;
        }
        // 注意基本类型可以直接比较,但引用类型最好调用其equal方法
        Card c = (Card) o;
        return rank == c.rank && suit.equals(c.suit);
    }
}

public class TestDemo1{
    public static void main(String[] args) {
        Card card1 = new Card(2, "♠");
        Card card2 = new Card(2, "♠");
        System.out.println(card1.equals(card2));
    }
}

运行结果:true

💥注意: 一般覆写 equals 的套路就是上面代码演示的

  • 如果指向同一个对象,返回 true
  • 如果传入的为 null,返回 false
  • 如果传入的对象类型不是 Card,返回 false
  • 按照类的实现目标完成比较,例如这里只要花色和数值一样,就认为是相同的牌
  • 注意下调用其他引用类型的比较也需要 equals,例如这里的 suit 的比较

重写基类equal的方式虽然可以比较,但缺陷是:equal只能按照相等进行比较,不能按照大于、小于的方式进行比较。

🌌三种方式对比:

🍓深拷贝与浅拷贝代码演示

  • 决定是深拷贝还是浅拷贝不是方法的用途,是代码的实现。

🌊浅拷贝代码示例:

class Money{
    public double m = 66.66;
}
class Person implements Cloneable{
    public int age;
    Money money = new Money();

    public void eat(){
        System.out.println("吃!");
    }

    @Override
    public String toString() {
        return "demo1.Person{" +
                "age=" + age +
                '}';
    }

    //重写 clone() 方法
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

public class TestDemo1  {
    public static void main(String[] args) throws CloneNotSupportedException {
        Person person1 = new Person();
        Person person2 = (Person) person1.clone();
        System.out.println(person1.money.m);
        System.out.println(person2.money.m);
        System.out.println("================");
        person2.money.m = 33.33;
        System.out.println(person1.money.m);
        System.out.println(person2.money.m);
    }
}

内存布局

运行结果

  • 只克隆了Person对象,并没有将Person对象中的Money对象克隆,所以是浅拷贝。

深拷贝:

  • 修改Person类中重写的clone()方法,对money对象进行克隆。
  • Money类实现Cloneable接口,在Money中重写clone()方法。

🌊将上面的代码变成深拷贝:

class Money implements Cloneable{
    public double m = 66.66;

    //重写 clone() 方法
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
class Person implements Cloneable{
    public int age;
    Money money = new Money();

    public void eat(){
        System.out.println("吃!");
    }

    @Override
    public String toString() {
        return "demo1.Person{" +
                "age=" + age +
                '}';
    }

    //重写 clone() 方法
    @Override
    protected Object clone() throws CloneNotSupportedException {
        Person tmp = (Person) super.clone();
        tmp.money = (Money) this.money.clone();
        return tmp;
    }
}

public class TestDemo1  {
    public static void main(String[] args) throws CloneNotSupportedException {
        Person person1 = new Person();
        Person person2 = (Person) person1.clone();
        System.out.println(person1.money.m);
        System.out.println(person2.money.m);
        System.out.println("================");
        person2.money.m = 33.33;
        System.out.println(person1.money.m);
        System.out.println(person2.money.m);
    }
}

内存布局

 运行结果

 16、抽象类和接口的区别

  • 抽象类和接口都是 Java 中多态的常见使用方式。
  • 抽象类中可以包含普通方法和普通字段, 这样的普通方法和字段可以被子类直接使用(不必重写), 而接口中不能包含普通方法, 子类必须重写所有的抽象方法。

  • 17
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 14
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

潇湘夜雨.

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

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

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

打赏作者

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

抵扣说明:

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

余额充值