Java面向对象编程

1. 类和对象

1.1. 面向对象编程

public class Person {
    String name;
    int age;
    public void eat() {
        System.out.println("eat");
    }
    public void work() {
        System.out.println("work");
    }
    public void rest(String site) {
        System.out.println(name + "在" + site + "休息");
    }
}
public class Test {
    public static void main(String[] args) {
        int n;
        n = 5;
        System.out.println(n);
        Person person1;
        person1 = new Person();
        person1.age = 23;
        person1.name = "zhangsan";
        person1.eat();
        person1.work();
        person1.rest("宿舍");
        Person person2 = new Person();
        person2.name = "lisi";
        person1.age = 24;
        person2.work();
        person2.eat();
        person2.rest("豪宅");
    }
}

内存分布图:
在这里插入图片描述

1.2. 局部变量

类中定义的变量是成员变量,而方法中定义的变量,包括方法的参数,代码中定义的变量被称为局部变量
在这里插入图片描述

1.3. 构造方法

对于一个类来说,有三种常见的成员:属性、方法和构造器,这三种成员都可以定义零个或多个
构造器:用于对象的初始化,是一个创建对象时被自动调用的特殊方法。构造器的名称应与类名一致。Java通过new关键字来调用构造器,从而返回该类的实例
声明格式:

[修饰符] 类名(形参列表) {
	// n条语句
}

1. 构造器的方法名必须和类名一致
2. 构造器通过new关键字调用
3. 构造器不能定义返回值类型(返回值类型肯定是本类),不能在构造器里使用return
4. 如果没有定义构造器,则编译器会自动定义一个无参的构造方法。

构造方法也是方法,与普通的方法一样,也可以重载

public class Computer {
    private String cpu = "Intel";
    private String memory;
    private String mainBoard;
    private String keyBoard;

    public Computer() {
        System.out.println("---computer---");
        cpu = "AMD";
    }
    public Computer(String cpu, String memory, String mainBoard, String keyBoard) {
        this.cpu = cpu;
        this.memory = memory;
        this.mainBoard = mainBoard;
        this.keyBoard = keyBoard;
    }
    public void start() {
        System.out.println("---start---");
    }
    public void close() {
        System.out.println("---close---");
    }
    public void show() {
        System.out.println("cpu=" + cpu + " memory=" + memory + " mainBoard=" + mainBoard + " keyBoard=" + keyBoard);
    }

    public static void main(String[] args) {
        Computer computer1 = new Computer();
        computer1.cpu = "酷睿";
        computer1.memory = "三星";
        computer1.keyBoard = "罗技";
        computer1.mainBoard = "华硕";
        computer1.start();
        computer1.show(); // cpu=AMD memory=三星 mainBoard=华硕 keyBoard=罗技
        computer1.close();
        Computer computer = new Computer("酷睿", "三星", "华硕", "罗技");
        computer.start();
        computer.show(); // 同上
        computer.close();
    }
}

易错问题:
Q:对象的创建完全是由构造方法实现的吗?
A:不完全是,构造方法通过new关键字调用构造器时,构造器也返回了该类对象,但这个对象并不是完全由构造器负责创建的。创建一个对象的四步:
1. 分配对象空间,并将对象成员变量初始化为0或空
2. 执行属性值的显示初始化
3. 执行构造方法
4. 返回对象的地址给相关的变量

注意:如果方法构造中形参名与属性名相同,需要使用this关键字区分属性与形参。this.id表示属性id,id表示形参id

1.4. 对象数组

public class TestArray {
    public static void main(String[] args) {
        int[] arr;
        arr = new int[4];
        arr[0] = 90;
        arr[1] = 80;
        arr[2] = 100;
        arr[3] = 54;
        for (int i : arr) {
            System.out.println(i);
        }
        System.out.println("===========");
        Computer[] arr2 = new Computer[4];
        arr2[0] = new Computer("酷睿", "三星", "华硕", "罗技");
        arr2[1] = new Computer("Inter", "金士顿", "华硕", "罗技");
        arr2[2] = new Computer("AMD", "三星", "华硕", "罗技");
        arr2[3] = new Computer("酷睿", "金士顿", "华硕", "罗技");
        for (Computer computer:arr2) {
            System.out.println(computer.toString());
            computer.show();
        }
    }
}

内存分配图:
在这里插入图片描述

2. 构造方法

2.1. 方法调用

2.1.1. 基本数据类型方法调用

public class TestPrimaryArgs {
    public static void main(String[] args) {
        int num1 = 10;
        int num2 = 20;
        System.out.println("交换前:num1: " + num1 + ",num2:" + num2); // 交换前:num1: 10,num2:20
        swap(num1, num2);
        System.out.println("交换后:num1: " + num1 + ",num2: " + num2); // 交换后:num1: 10,num2: 20
    }
    public static void swap(int num1, int num2) {
        int temp = num1;
        num1 = num2;
        num2 = temp;
        System.out.println("交换后inner:num1: " + num1 + ",num2: " + num2); // 交换后inner:num1: 20,num2: 10
    }
}

2.1.2. 引用数据类型方法调用

public class Point {
    int x;
    int y;
}

public class TestRefArgs {
    public static void main(String[] args) {
        Point p = new Point();
        p.x = 10;
        p.y = 20;
        System.out.println("交换前:p.x=" + p.x + ",p.y=" + p.y); // 交换前:p.x=10,p.y=20
        swap(p);
        System.out.println("交换后:p.x=" + p.x + ",p.y=" + p.y); // 交换后:p.x=20,p.y=10
    }
    public static void swap(Point p) {
        int temp = p.x;
        p.x = p.y;
        p.y = temp;
    }
}

2.1.3. 总结

基本数据类型的参数是值传递,引用数据类型的参数是传递的是引用(地址),本质上也是值传递。

2.2. this

创建一个对象分为四步:

  1. 分配对象空间,并将对象成员变量初始化为0或空
  2. 执行属性值的显示初始化
  3. 执行构造方法
  4. 返回对象的地址给相关的变量

this的本质就是创建好的对象的地址,由于在构造方法调用前,对象已经创建。因此,在构造方法中this可以代表当前对象

this的常用用法:

  • 调用成员变量:如果成员变量和局部变量同名,this用来区分两者;如果没有同名的局部变量,this可以不写
  • 调用成员方法:this可省
  • 调用构造方法:使用this关键字调用重载的构造方法,避免相同的初始化代码。但只能在构造方法中用,并且必须位于构造方法的第一句
  • this不能用于static方法中
public class Student {
    private int sno;
    private String name;
    private String sex;
    private double score;

    public Student() {}

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

    public Student (int sno, String name, String sex, double score) {
        this(sno, name, sex);
        this.score = score;
    }
    private void  study() {
        this.shout(); // 1. 好好学习,天天向上
        shout(); // 2. 好好学习,天天向上
        System.out.println("努力学习"); // 3. 努力学习
    }
    public void shout() {
        System.out.println("好好学习,天天向上"); // 4. 好好学习,天天向上
    }
    public void show() {
        System.out.println(sno+"一" + this.name + "一" + sex + "一" + this.score); // 5. 7一wyb一男一27.0
    }

    public static void main(String[] args) {
        Student stu = new Student(7, "wyb", "男", 27);
        stu.study();
        stu.shout();
        stu.show();
        Student stu2 = new Student();
    }
}

3. static关键字

一个类的成员包括变量、方法、构造方法、代码块和内部类,static可以修饰除了构造方法以外的所有成员。
使用static修饰的成员成为静态成员,是属于某个类的而不使用static修饰的成员是实例成员,是属于类的每个对象的

3.1. static变量

在类中,用static声明的成员变量为静态成员变量,也称类变量。静态成员变量的生命周期和类相同,在整个应用程序执行期间都有效。特点如下:

  • 为该类的公用变量,属于类,被该类的所有实例共享,在类被载入时显式初始化
  • 对于该类的所有对象来说,static成员变量只有一份,被该类的所有对象共享
  • 一般用"类名.类属性/方法"来调用。(也可以通过对象引用或类名(不需要视力化)访问静态成员)
  • 在static方法中不可直接访问非static的成员
public class Student {
    private String cup;
    private static String classRoom;

    public Student() {}

    public Student(String cup) {
        this.cup = cup;
    }
    public void show() {
        System.out.println(cup + "一" + classRoom); // 迪士尼一null
    }

    public static void main(String[] args) {
        Student stu = new Student("迪士尼");
        stu.show();
        System.out.println(Student.classRoom); // null
        Student.classRoom = "332";
        System.out.println(Student.classRoom); // 332
    }
}

总结:static变量和非static变量的区别:
1. 份数不同:静态变量1份;非静态变量,一个对象一份
2. 存储位置不同:静态变量存储在方法区;非静态变量存储在堆中
3. 内存分配空间的时间不同:静态变量在第一次加载类的时候分配空间;非静态变量在创建对象时分配空间
4. 生命周期不同:静态变量和类的生命周期相同;非静态变量的生命周期和所属对象相同
5. 调用方式不同
静态变量:通过类名调用 Student.classRomm;也可以通过对象名调用stu.classRoom = “332”,不推荐;非静态变量:通过对象名调用 stu.name = “wyb”

3.2. static方法

  • static方法的作用:访问static变量和static方法
  • static方法的调用方式:通过类名调用:Student.showClassRoom(),推荐该方式;通过对象名访问:stu.showClassRoom();
  • 静态方法中不可以访问非静态变量、非静态方法和this;理解:加载类的时候就加载静态变量和静态方法,此时可能还没创建对象,所以非静态变量和非静态方法还没有分配空间,无法访问
  • 非静态方法中可以访问静态变量、静态方法;理解:加载类时就已经加载静态变量和静态方法了,创建对象后,非静态变量和非静态方法才分配空间,此时静态变量和静态方法已经存在,可以访问
public class Student2 {
    private String cup;
    private static String classRoom;
    public static void showClassRoom() {
        System.out.println(classRoom);
        /*System.out.println(cup); // 非静态变量,不可访问
        show(); // 非静态方法,不可访问
        System.out.println(this); // this,不可访问*/
    }
    public static void setClassRoom(String classRoom) {
        Student2.classRoom = classRoom;
    }

    public Student2() {}

    public Student2(String cup) {
        this.cup = cup;
    }
    public void show() {
        System.out.println(cup + "一" + classRoom); // 3. 迪士尼一614
        showClassRoom(); // 4. 614
    }

    public static void main(String[] args) {
        Student2.showClassRoom(); // 1. null
        Student2.setClassRoom("504");
        Student2.showClassRoom(); // 2.504
        Student2 stu = new Student2("迪士尼");
        stu.classRoom = "614";
        stu.show();
        stu.showClassRoom(); // 5. 614
    }
}

3.3. static代码块

  • 局部代码块:
    • 位置:方法中
    • 数量:多个
    • 执行顺序:依次执行
    • 局部代码块中定义的变量作用范围只限于当前代码块
  • (成员)代码块
    • 位置:类中
    • 数量:多个
    • 执行顺序:依次执行
    • 执行时间:每次创建对象时都执行;先执行代码块,再执行构造方法
    • 作用:实际开发中很少用;可以将各个构造方法中公共的代码提取到代码块;匿名内部类不能提供构造方法,此时初始化操作放到代码块中
  • static代码块
    • 位置:类中
    • 数量:多个
    • 执行顺序:依次执行
    • 执行时间:第一次加载类时执行,只执行一次
    • 作用:给静态变量赋初始值。实际开发中使用较多,一般用于执行一些全局性的初始化操作,比如创建工厂、加载数据库初始信息
public class Student3 {
    static  {
        System.out.println("static code block1"); // 1. static code block1
    }
    String name;
    static String classRoom;
    static  {
        // name = "wyb";
        classRoom = "332";
        System.out.println("static code block2"); // 2. static code block2
    }
    public Student3() {
        System.out.println("-------Student3-------"); // 6. -------Student3-------
    }

    public static void main(String[] args) {
        int n = 4; // 局部变量
        // 局部代码块
        {
            int m = 5;
            System.out.println(m); // 3. 5
        }
        System.out.println(classRoom); // 4. 332
        new Student3();
    }
    {
        classRoom = "504";
        System.out.println("code block3"); // 5. code block3
    }
}

4. package和import

4.1. package包

包机制时java中管理类的重要手段。开发中,我们会遇到大量同名的类,通过包可以解决类重名的问题,也可以实现对类的有效管理。还可以和访问权限有密切关系。

  • 定义包
    • 包名:域名倒着写,再加上模块,便于内部管理类
    • 包名一律小写
  • 使用包:通常是类第一句非注释性语句,必须以;结尾
  • java常用包
    在这里插入图片描述
    注意:
    • 写项目时都要加包,不要使用默认包
    • com.gao和com.gao.car,这两个包没有包含关系,是两个完全独立的包。只是逻辑上看起来后者是前者的一部分。

4.2. import导入

使用其他包的类,需要使用import导入,从而可以在本类中直接通过类名来调用,否则就需要书写类的完整包名和类名。import后,便于编写代码,提高可维护性。
注意:

  • 默认是当前包的类和接口
  • java会默认导入java.lang包下的所有类,因此这些类可以直接使用
  • 可以使用通配符,比如import com.baidu.oop.object.*;会导入该包下所有类和接口(但不包括下级包)
  • 如果导入两个同名的类,只能用包名+类名来显示调用相关类:java.util.Date date = new java.util.Date();
    使用静态导入static import:可以导入指定类的静态属性和静态方法
import java.lang.reflect.Constructor;
import java.util.Date;
import java.util.*;
import static java.lang.Math.PI;
import static java.lang.Math.*;
public class TestImport {
    public static void main(String[] args) {
        Date date = new Date();
        java.sql.Date date1 = java.sql.Date.valueOf("1991-10-06");
        List list;
        Set set;
        Map map;
        Constructor constructor;
        System.out.println(PI);
        System.out.println(sqrt(64));
        System.out.println(pow(7,2));
    }
}

5. 封装

封装是面向对象三大特征之一。对于程序合理的封装让外部调用更加方便,更加利于协作,同时,同于实现者来说也更加容易修正和改版代码

5.1. 封装优点

  • 提高代码的安全性
  • 提高代码的复用性
  • 高内聚:封装细节,便于修改内部代码,提高可维护性
  • 低耦合:简化外部调用,便于调用者使用,便于扩展和维护

5.2. 权限修饰符

使用权限修饰符可以尽可能的让访问权限降到最低,从而提高安全性。

在这里插入图片描述

  1. private表示私有,只有自己类能访问
  2. default表示没有修饰符修饰,只有同一个包的类能访问
  3. protected表示可以被同一包的类以及其他包中的子类访问
  4. public表示可以被该项目的所有包中的所有类访问
  • 类成员的处理:
    • 一般使用private访问权限
    • 提供相应的get/set方法来访问相关属性,这些方法通常是public修饰的,以提供对属性的赋值与读取操作(注意:boolean变量的get方法是is开头的)
    • 一些只用于本类的辅助性方法可以用private修饰,希望其他类调用的方法用public修饰
  • 类的处理:
    • 类只能使用public和默认来修饰
    • 默认:当前包
    • public:当前项目的所有包
    • public类要求类名和文件名相同,一个java文件中至多一个public类
public class Person {
    private String name;
    private int age;
    public Person() {}
    public Person(String name, int age) {
        this.name = name;
        setAge(age);
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
    public void setAge(int age) {
        if(age > 130 || age < 0) {
            this.age = 18;
        }else {
            this.age =age;
        }
    }
    public int getAge() {
        return age;
    }
    @Override
    public String toString() {
        return "Person [name=" + getName() + ", age=" + getAge() + "]";
    }
}
public class Test {
    public static void main(String[] args) {
        Person p1 = new Person();
        p1.setName("wyb");
        p1.setAge(27);
        System.out.println(p1); // Person [name=wyb, age=27]
        Person p2 = new Person("xz", 32);
        System.out.println(p2); // Person [name=xz, age=32]
    }
}

6. 继承

继承是面向对象编程三大特征之一,可以更加容易实现对已有类的扩展和对现实世界的建模

6.1. 继承及其作用

继承更加容易实现了类的扩展。实现了代码的重用

public class Animal {
    private String color;
    private int age;
    public Animal() {
        super();
    }
    public Animal(String color, int age) {
        this.color = color;
        this.age = age;
    }
    public void eat() {
        System.out.println("eat");
    }
    public void show() {
        System.out.println(color + " " + age);
    }
}
public class Dog extends Animal {
    private String nickName;
    public Dog() {
    }
    public Dog(String color, int age) {
    }
    public Dog(String color, int age, String nickName) {
        super(color, age);
        this.nickName = nickName;
    }
    public void guard() {
        System.out.println("wang wang");
    }
}
public class Cat extends Animal{
    private int eyeSight;
    public Cat() {
        super();
    }
    public Cat(String color, int age, int eyeSight) {
        super(color, age);
        this.eyeSight = eyeSight;
    }
    public void grabMouse() {
        System.out.println("vat grad mouse");
    }
}
public class Test {
    public static void main(String[] args) {
        Dog dog = new Dog("黑色",3, "大黄");
        dog.guard(); // wang wang
        dog.show(); // 黑色 3
        Cat cat = new Cat("小花",12, 5);
        cat.grabMouse(); // vat grad mouse
        cat.show(); // 小花 12
    }
}
  • 继承的使用要点:
    • 父类也称超类、基类,子类称为派生类
    • java中只有单继承,没有多继承,多继承会引起混乱,使得继承链过于复杂,系统难以维护
    • 子类继承父类,可以得到父类的全部属性和方法(父类的构造方法除外),但不一定可以直接访问(父类的私有属性和方法不可以访问)
    • 如果定义一个类时,没有调用extends,则他的父类是:java.lang.Object

6.2. 方法重写

父类的方法无法满足子类的需求,可通过方法重写(override)解决

@Override
    public String toString() {
        // return super.toString();
        return "Animal[color=" + color + ", age=" + age + "]";
    }

6.3. 总结

方法重载overload和重写 override是面向对象的两个重要概念,下面是他们之间的区分:

  • 总体区别:
    • 重载:位于同一个类中,在一个类里为一种行为提供多种实现方式并提高可读性
    • 重写:位于子类和父类间,父类方法无法满足子类的要求,子类通过方法重写满足要求
  • 细节区别:
    • 重载:与修饰符,返回值,抛出异常无关,方法名要相同,参数不同
    • 重写:子类修饰符>=父类,子类返回值<=父类,方法名,参数相同,子类抛出异常<=父类

重载实例:构造方法重载、println()方法重载
重写实例:Object类的toString()、equals()、hashCode()等都可以被子类重写
注意:

  1. 某些方法使用final修饰,将无法被重写。比如Object类的wait()、notify()等
  2. 静态方法无法进行方法重写。如果父类中含有一个静态方法,且在子类中也含有一个返回类型、方法名、参数列表均与之相同的静态方法,那么该子类实际上只是将父类中的该同名方法进行了隐藏,而非重写,添加@override注解会报错。

7. 继承

7.1. Object类

Object类是所有java类的根基类,所有的java对象都拥有Object类的属性和方法。如果在类的声明中未使用extends关键字指明父类,则默认继承Object类
面试题:写出Object类的6个方法:

  • Object的public方法

    • equals(Object obj):指示其他某个对象是否与此对象相等,返回boolean
    • getClass():返回此Object的运行时类
    • hashCode():返回该对象的哈希码值
    • notify():唤醒在此对象监视器上等待的单个线程
    • notifyAll():唤醒在此对象监视器上等待的所有线程
    • toString():返回该对象的字符串表示
    • wait():在其他线程调用此对象的notify()方法或notifyAll()方法前,导致当前线程等待
    • wait(long timeout):在其他线程调用此对象的notify()方法或notifyAll()方法,或其他某个线程中断当前线程,或者已超过某个实际时间量前,导致当前线程等待
  • Object的private、protected方法

    • clone():创建并返回此对象的一个副本
    • finalize():当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法
  • native关键字:

    • 一个native方法就是一个java调用非java代码的接口,该方法不是由java实现的,而是由c或c++实现的
    • 定义一个native方法,并不提供实现体。

7.2. 成员变量的隐藏

如果父类和子类中有同名的成员变量,不存在变量的重写,分别占有自己的空间。子类的成员变量优先,称为成员变量的隐藏。

public class Animal {
    String color = "Animal的color";
    int age;
    public String getColor() {
        return color;
    }
}
public class Dog extends Animal {
    String color = "Dog的color";
    String nickName;
    public String getColor() {
        return color;
    }
    public String getSuperColor() {
        return super.getColor();
    }
    public void show() {
        String color = "方法的color";
        System.out.println(color); // 方法的color
        System.out.println(this.color); // Dog的color
        System.out.println(super.color); // Animal的color
    }

    public static void main(String[] args) {
        Dog dog = new Dog();
        dog.show();
        System.out.println(dog.getColor()); // Dog的color
        System.out.println(dog.getSuperColor()); // Animal的color
    }
}

7.3. 继承情况下构造方法的调用过程

  • 继承条件下构造方法的执行顺序
    • 构造方法的第一条语句默认是super(),含义是调用父类无参数的构造方法
    • 构造方法的第一条语句可以显式的指定为父类的有参数构造方法:super(…)
    • 构造方法的第一条语句可以显式的指定为当前类的构造方法:this(…)
  • 注意
    • 每个类最好要提供无参数的构造方法
    • 构造方法的第一条语句可以是通过super或this调用构造方法,须是第一条语句
    • 构造方法中不能同时使用super和this调用构造方法,并不是说不能同时出现this和super
public class Animal1 {
    String color;
    private int age;
    public Animal1() {
        super();
    }
    public Animal1(String color, int age) {
        this.color = color;
        this.age = age;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
}
public class Dog1 extends Animal1{
    private String nickName;
    private String type;
    public Dog1() {
        super();
    }
    public Dog1(String color,int age, String nickName) {
        super(color, age);
        this.nickName = nickName;
    }
    public Dog1(String color,int age, String nickName, String type) {
        this(color, age, nickName);
        this.type = type;
    }
    public String toString() {
        return  this.color + " " + this.getAge() + " " + this.nickName + " " + this.type;
    }

    public static void main(String[] args) {
        Dog1 dog1 = new Dog1("黑色",3,"旺财","泰迪");
        System.out.println(dog1.toString()); // 黑色 3 旺财 泰迪
    }
}

7.4. super关键字

super可以看作是直接父类对象的引用。每一个子类对象都会有一个super引用指向其直接父类对象

  • 使用super可以:
    1. 调用成员变量:super.color
    2. 调用成员方法:super.introduce();
    3. 调用构造方法:super(color, age);
  • 注意:
    1. 使用super调用普通方法,语句没有位置限制,可以在子类中随便调用
    2. 在一个类中,若是构造方法的第一行代码没有显示的调用super(…)或者this(…);那么java默认都会调用super(),含义是调用父类的无参数构造方法。这里的super可以省略

7.5. ==和equals方法

==:代表比较双方是否相同。如果是基本类型则表示值相等,如果是引用类型则表示地址相等
equals:默认比较两个对象的hashcode
,是同一个对象的引用时返回true,否则返回false。显然,这无法满足子类的要求,可根据要求重写equals方法
重写equals方法:

public class Animal2 {
    String color;
    private int age;
    public boolean equals(Object obj) {
        Animal2 other = (Animal2) obj;
        if(obj == null) return false;
        // 如果两个变量指向同一个空间,直接返回true
        if(this == obj) return true;
        if(this.color.equals(other.color) && this.age == other.age) {
            return true;
        }else {
            return false;
        }
    }
}
public class Dog2 extends Animal2{
    private String nickName;
    private String type;
    public boolean equals(Object obj) {
        Dog2 other = (Dog2) obj;
        boolean flag = super.equals(obj);
        if(!flag) {
            return false;
        }else {
            // 如果Animal2的equals返回true,需要再比较nickName、type
            if(this.nickName.equals(other.nickName) && this.type.equals(other.type)) {
                return true;
            }else {
                return false;
            }
        }
    }
}

7.6. 组合

继承和组合是复用代码的两种方式

  • 继承:is-a,Dog is a Animal, Cat is a Animal
  • 组合:has-a,Computer has a cup memery mainBoard

面向对象的设计原则之一:组合聚合复用原则(优先使用组合,而不是继承)

  • 除非两个类之间是is-a的关系,否则不要轻易使用继承;过多的继承会破坏代码的可维护性,因为父类被修改会影响到所有继承自他的子类
  • 如果类之间没有is-a的关系,可以通过实现接口与组合的方式来达到相同的目的。采用接口与组合的方式具有更好的扩展性
public class Cpu {
    private String name;
    private double rate;
    public void calc() {
        System.out.println("cpu calc...");
    }
    public void control() {
        System.out.println("cpu control...");
    }
}
public class AMDCpu extends Cpu{
    public void calc() {
        System.out.println("AMD calc");
    }
}
public class IntelCpu extends Cpu{
    public void calc() {
        System.out.println("IntelCpu calc");
    }
}
public class MainBoard {
    public void connect() {
        System.out.println("main board connected");
    }
}
public class Memory {
    public void process() {
        System.out.println("memory process");
    }
}
public class Computer {
    private Cpu cpu = new Cpu();
    private Memory memory = new Memory();
    private MainBoard mainBoard = new MainBoard();
    private void setCpu(Cpu cpu) {
        this.cpu = cpu;
    }
    public void computer() {
        cpu.calc(); // AMD calc
        cpu.control(); // cpu control...
        memory.process(); // memory process
        mainBoard.connect(); // main board connected
    }

    public static void main(String[] args) {
        Computer computer = new Computer();
        Cpu cpu = new AMDCpu();
        Cpu cpu1= new IntelCpu();
        Cpu cpu2= new Cpu();
        computer.setCpu(cpu);
        computer.computer();
    }
}

8. 多态

多态是面向对象三个特征之一。同一行为,通过不同的子类,可以体现出来的不同的形态

8.1. 引入和使用多态

多态指的是同一个方法调用,由于对象不同可能会有不同的行为。

  • 编译期类型:=左边的类型
  • 运行期类型:=右边的类型

当有继承关系时,可能发生编译期类型和运行期类型不同的情况,即编译期类型是父类类型,运行期类型是子类类型:父类引用指向子类对象

  • 多态的要点
    • 多态是方法的多态,不是属性的多态(多态与属性无关)
    • 多态存在的3个必要条件:继承,方法重写,父类引用指向子类对象
    • 父类引用指向子类对象后,用该父类引用调用子类重写的方法,此时多态就出现了
public class Programmer {
    public String name = "proName";
    // 1. 子类继承的
    public void writeCode() {
        System.out.println("writing code...");
    }
    // 2. 子类重写的
    public void eat() {
        System.out.println("eat...");
    }
}
public class Chinese extends Programmer{
    public String name = "ChinaName";
    // 1. 从父类继承一个方法writeCode();
    // 2. 重写父类的一个方法eat
    public void eat() {
        System.out.println("chinese eat");
    }
    // 3. 子类特有的方法
    public void playShadowBoxing() {
        System.out.println("chinese playShadowBoxing");
    }
}
public class English extends Programmer{
    // 1. 从父类继承一个方法writeCode
    // 2. 重写父类的方法eat
    public void eat() {
        System.out.println("english eat");
    }
    // 3. 子类特有的方法
    public void raceHorse() {
        System.out.println("english race");
    }
}
public class Test {
    public static void showEat(Programmer pro) {
        pro.eat();
    }

    public static void main(String[] args) {
        Programmer ch = new Chinese();
        showEat(ch); // chinese eat
        English en = new English();
        showEat(en); // english eat
        Programmer pro = new Programmer();
        showEat(pro); // eat...
    }
}
public class Test1 {
    public static void main(String[] args) {
        Chinese ch = new Chinese();
        ch.writeCode(); // writing code...
        ch.eat(); // chinese eat
        ch.playShadowBoxing(); // chinese playShadowBoxing
        Programmer pro = new Programmer();
        pro.writeCode(); // writing code...
        pro.eat(); // eat...
    }
}

使用父类做方法的行参,是多态使用最多的场合。即使增加了新的子类,方法也无需改变,符和开闭原则。
父类引用做方法的行参,实参可以是任意的子类对象,可以通过不同的子类对象实现不同的行为方式。即使增加了新的子类,方法也无需改变,提高了扩展性,符和开闭原则。

  • 面线对象的设计原则之一:开闭原则OCP
    • 对扩展开放,对修改关闭
    • 软件系统中的各种组件应该在不修改现有代码的基础上,引入新功能

8.2. 多态之向上转型

将子类对象赋给父类引用,称为向上转型,自动进行类型转换。向上转型可以调用子类继承的方法,但不能调用子类特有的方法。如果子类重写了父类的方法,向上转型后通过父类引用调用的是真实子类重写的方法。

public class TestPoly {
    public static void main(String[] args) {
        // 基本数据类型的自动转换
        int n = 10;
        System.out.println(n); // 10
        // 左 > 右 自动转换
        double d = n;
        System.out.println(d); // 10.0
        // 引用数据类型自动转换
        Programmer programmer = new Chinese(); // 自动转换,向上转型 左 > 右
        programmer.writeCode(); // writing code...
        programmer.eat(); // chinese eat
    }
}

8.3. 向下转型

将父类的引用变量转换为子类类型,称为向下转型。向下转型后就可以调用子类特有的方法了

  • 需要进行强制转换:Chinese ch = (Chinese)pro;
  • 强制转换必须转换成真实子类型,否则报错ClassCaseException
  • 向下转型之间肯定发生了向上转型
  • 为了避免ClassCaseException,向下转型之前使用instanceof先判断一下
    • pro instanceof Chinese 对象 instanceof 类/接口
    • 使用instanceof的前提:左边的对象和右边的类型在继承树上有上下级关系
public class TestPoly1 {
    public static void main(String[] args) {
        double d = 3.14;
        System.out.println(d); // 3.14
        int n = (int)d;
        System.out.println(n); // 3
        Programmer programmer = new English();
        // 多态
        programmer.eat(); // chinese eat
        // 左 < 右,必须转换成原来的真实子类型
        // Chinese ch = (Chinese) programmer;
        // ch.playShadowBoxing(); // java.lang.ClassCastException:
        if(programmer instanceof Chinese) {
            Chinese ch1 = (Chinese) programmer;
            ch1.playShadowBoxing();
        }else {
            English en = (English) programmer;
            en.raceHorse();
        }
        System.out.println(programmer instanceof Chinese); // false
        System.out.println(programmer instanceof Programmer); // true
        System.out.println(programmer instanceof Object); // true
    }
}

注意:多态和方法有关,与属性无关

public class TestPoly2 {
    public static void main(String[] args) {
        Chinese chinese = new Chinese();
        System.out.println(chinese.name); // ChinaName
        Programmer program = new Programmer();
        System.out.println(program.name); // proName
        Programmer program2 = new Chinese();
        System.out.println(program2.name); // proName
        program2.eat(); // chinese eat
    }
}

8.4. 简单工厂模式-返回值是父类类型

不仅可以使用父类做方法的行参,还可以使用父类做方法的返回值类型,真实返回的对象可以是该类的任意一个子类对象

public class WybSchool {
    public static Programmer getProgrammer(String type) {
        Programmer pro = null;
        if("ch".equals(type)) {
            pro = new Chinese();
        }else {
            pro = new English();
        }
        return pro;
    }
}
public class TestPoly3 {
    public static void main(String[] args) {
        Programmer pro = WybSchool.getProgrammer("ch");
        pro.eat(); // chinese eat
    }
}

工厂模式是解决大量对象创建问题的一个解决方案。将创建和使用分开,工厂负责创建,使用者直接调用即可,简单工厂模式的基本要求:

  • 定义一个static方法,通过类名直接调用
  • 返回值类型是父类类型,返回的可以是其任意子类类型
  • 传入一个字符串类型的参数,工厂根据参数创建对应的子类

9. final和抽象类

9.1. final

  • final关键字的作用:
    • 修饰变量:被final修饰的变量不可改变。一旦赋初值,不可重新赋值。final int MAX_SPEED = 120;
    • 修饰方法:该方法不可被子类重写,但是可以重载。final void study();
    • 修饰类:修饰的类不能被继承。比如:Math、String、System;final class A();
public class Maths {
    private Maths() {
    }
    public static final double PI = 3.14159;
    public static final double pow(int x, int y) {
        double result = 1;
        for (int i = 0; i < y; i++) {
            result *= x;
        }
        return result;
    }
    public static double abs(double num) {
        if(num >= 0) {
            return num;
        }else {
            return -num;
        }
    }
}
  • 注意:
    • final不能修饰构造方法
    • final修饰基本数据类型,值只能赋值一次
    • final修饰引用数据类型,final Dog dog = new Dog(“大黄”); 不能变化的引用变量的值,可以变化的是对象的属性
public class Dog {
    String name;
    public Dog(String name) {
        this.name = name;
    }
}
public class Test {
    public static void main(String[] args) {
        final int NUM;
        NUM = 5;
        // NUM = 6;
        System.out.println(NUM); // 5
        final Dog dog;
        dog = new Dog("大黄");
        dog.name = "旺财";
        System.out.println(dog.name); // 旺财
    }
}

9.2. 抽象类

  • 抽象方法:使用abstract修饰的方法,没有方法体,只有声明。定义的是一种规范,就是告诉子类必须要给抽象方法提供具体的实现
  • 抽象类:使用abstract修饰的类。要求子类必须定义具体实现,通过抽象类,就可以做到严格限制子类的设计,使子类更加通用
public abstract class Animal {
    private String color;
    public Animal() {
    }
    public Animal(String color) {
        this.color = color;
    }
    public abstract void eat();
    public String toString() {
        return "Animal{color=" + color  + '\'' + "}";
    }
}
public class Dog1 extends Animal{
    private String nickName;
    public Dog1() {}
    public Dog1(String color, String nickName) {
        super(color);
        this.nickName = nickName;
    }
    public void eat() {}
    public String toString() {
        return "Dog1{nickName-" + nickName + '\'' +"}" + super.toString();
    }
}

抽象类的使用要点:

  • 有抽象方法的类只能定义成抽象类
  • 抽象类不能实例化,不能用new来创建抽象类
  • 抽象类必须有构造方法,创建子类对象的时候使用
  • 一个抽象类至少有0个抽象方法,至多所有方法都是抽象方法
  • 子类必须重写父类的抽象方法,不重写就会编译错误;或者子类也定义为抽象类

10. 接口

接口就是定义一组规则,是一种规范

10.1. 接口

  • 声明格式:
[访问修饰符] interface 接口名 [extends 父接口1, 父接口2...] {
	常量定义;
	方法定义;
}
  • 定义接口详细说明:
    • 访问修饰符:只能是public或默
    • 接口名:和类名命名机制相同
    • extends:接口可以多继承
    • 常量:接口中的属性只能是常量,总是:public static final修饰,不写也是
    • 方法:接口中的方法只能是:public abstract。省略也是
  • 要点:
    • 子类通过implements实现接口中的规范
    • 接口不能创建实例,但是可用于声明引用变量类型
    • 一个类实现了接口,必须实现接口中的所有方法,并且这些方法只能是public的
    • jdk1.8之前,接口中只能包含静态常量和抽象方法,不能有普通属性、构造方法和普通方法
    • jdk1.8后,接口中包含普通的静态方法和默认方法
public interface Flyable {
    public abstract void fly();
}
public class Plane implements Flyable{
    public void fly() {
        System.out.println("plane fly");
    }
}
public class Animal {
}
public class Bird extends Animal implements Flyable{
    public void fly() {
        System.out.println("bird fly");
    }
}
public class Test {
    public static void showFly(Flyable fly) {
        fly.fly();
    }
    public static void main(String[] args) {
        Flyable plane = new Plane();
        showFly(plane); // plane fly
        Bird bird = new Bird();
        showFly(bird); // bird fly
    }
}
  • 接口的组成
    • 接口和数组、类、抽象类都是同一个层次的概念
    • 成员变量:接口中所有变量都使用public static final修饰,都是全局静态常量
    • 成员方法:接口中所有方法都使用public abstract修饰,都是全局抽象方法
    • 构造方法:接口不能new,也没有构造方法
    • 接口做方法的行参,实参可以接口的所有实现类
  • 接口的多继承
    • c++多继承
      • 好处:从多个父类继承更多的功能
      • 缺点:不安全,如果有两个父类,都有相同的方法,子类如果没有重写,使用哪个方法呢?
    • java单继承
      • 好处:安全
      • 缺点:功能受限
      • 解决方案:采用接口,接口变相的使java实现了c++的多继承,又没有c++多继承的不安全性
      • 必须先extends再implements

10.2. 接口应用:内部比较器Comparable

定义一个比较接口Comparable,其中定义一个比较方法compareTo(Object obj),让各个类实现该接口

public interface Comparable {
    /*
    * 判断两个对象的大小
    * @param obj 另外一个对象
    * @return
    *   > 0  正数 大于
    *   = 0  等于
    *   < 0  负数 小于
    * */
    public int compareTo(Object obj);
}
public class Book implements Comparable{
    private String bookName;
    private String author;
    private String publisher;
    private double price;

    public Book(String bookName, String author, String publisher, double price) {
        this.bookName = bookName;
        this.author = author;
        this.publisher = publisher;
        this.price = price;
    }

    @Override
    public int compareTo(Object obj) {
        Book other = (Book) obj;
        if(this.price > other.price) {
            return 1;
        }else if(this.price < other.price) {
            return -1;
        }else {
            return 0;
        }
    }
}
public class Person implements Comparable{
    private String name;
    private int age;

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

    @Override
    public int compareTo(Object obj) {
        Person other = (Person) obj;
        return this.age - other.age;
    }
}
public class Test1 {
    public static void main(String[] args) {
        Book book1 = new Book("我是猫", "外国", "中信", 35);
        Book book2 = new Book("我是猫", "外国", "中信", 30);
        // boolean result = book1.equals(book2); // false
        int result = book1.compareTo(book2); // 1
        System.out.println(result);
        Person person1 = new Person("wyb", 27);
        Person person2 = new Person("xz", 32);
        result = person1.compareTo(person2); // -5
        System.out.println(result);
    }
}

10.3. JDK1.8的接口新特性

  • JDK7及其之前
    • 接口的变量都是public final static全局静态常量,无变化
    • 接口中都是抽象abstract方法,不能有static方法(因为abstract和static、final、private不能共存)
  • JDK1.8及其之后
    • 接口中可以添加非抽象方法static,实现类不能重写,只能通过接口名调用
    • 如果子类中定义了相同名字的静态方法,那就直接从属于子类,可以通过子类名直接调用
    • 接口中可以添加非抽象方法default,实现类可以重写,只能通过对象名调用
    • 实现类可以直接使用default方法,也可以重写default方法,但是必须去掉default
    • 上级接口中default方法的调用:MyInterface.super.method()
  • 提供非抽象方法的目的
    • 为了解决实现该接口的子类代码重复的问题
    • 为了java类库的类增加新功能,且不必对这些类重新进行设计。
public interface MyInterface {
    public static final double PI = 3.14;
    public abstract void method1();
    public static void method2(){
        System.out.println("jdk1.8中,非抽象方法static");
    }
    public default void method3() {
        System.out.println("jdk1.8中,非抽象方法default");
    }
    public static void main(String[] args) {
        MyInterface.method2();
    }
}
public class MyClass implements MyInterface{
    @Override
    public void method1() {
        System.out.println("接口中的抽象方法,子类必须实现");
    }
    @Override
    public void method3() {
        MyInterface.method2(); // jdk1.8中,非抽象方法static
        MyInterface.super.method3(); // jdk1.8中,非抽象方法default
        System.out.println("重写接口的default方法,须将default去掉"); // 重写接口的default方法,须将default去掉
    }
    public static void main(String[] args) {
        MyInterface mi = new MyClass();
        mi.method1(); // 接口中的抽象方法,子类必须实现
        MyInterface.method2(); // jdk1.8中,非抽象方法static
        mi.method3();
    }
}

10.4. 面向接口编程

面向接口编程是面向对象编程的一部分
接口就是规范,就是项目中最稳定的核心。面向接口编程可以把握住真正核心的东西,使实现复杂多变的需求成为可能。
通过面向接口编程,而不是面向实现类编程,可以大大降低程序模块间的耦合性,提高整个系统的可扩展性和可维护性

11. 内部类

内部类是一类特殊的类,指的是定义在一个类的内部的类,为了方便的使用外部类的相关属性和方法,这时候通常会定义一个内部类。
内部类主要分为:非静态成员内部类、静态成员内部类、局部内部类、匿名内部类

11.1. 非静态成员内部类

作为类的成员存在,和成员变量、成员方法、构造方法、代码块并列。因为是类的成员,所以非静态成员内部类可以使用public、protected、默认、private修饰,而外部类只能使用public、默认修饰

public class OuterClass {
    private String name;
    private int num = 10;
    public OuterClass() {}
    public OuterClass(String name, int num) {
        this.name = name;
        this.num = num;
    }
    public void methodOut() {
        System.out.println("method");
    }
    public void methodOut1() {
        // 外部类不可以直接访问内部的成员变量和成员方法
        // System.out.println(type);
        // methodInner();
        InnerClass ic = new InnerClass();
        System.out.println(ic.num); // 20
        ic.methodInner(); // null 30 20 10 method
    }
    // 内部类
    class InnerClass {
        private String type;
        private int num = 20;
        public InnerClass() {}
        public InnerClass(String type, int num) {
            this.type = type;
            this.num = num;
        }
        public void methodInner() {
            // 内部类可以访问外部类的成员变量
            System.out.println(name); // null
            int num = 30;
            System.out.println(num); // 30
            System.out.println(this.num); // 20
            // 内部类访问外部类的同名成员变量
            System.out.println(OuterClass.this.num); // 10
            methodOut();
        }
    }
}
public class Test {
    public static void main(String[] args) {
        OuterClass oc = new OuterClass();
        oc.methodOut();
        oc.methodOut1();
        // 要创建非静态成员内部类对象,必须先创建外部类的对象
        // OuterClass.InnerClass ic = new OuterClass().new InnerClass();
        OuterClass oc1 = new OuterClass();
        OuterClass.InnerClass ic = oc1.new InnerClass();
    }
}
  • 基本特征:
    • 内部类可以直接访问外部类的成员
    • 外部类不能直接访问内部类的成员,需要先创建对象再通过对象名访问
    • 内部类访问外部类的同名成员变量:OuterClass.this.num
    • 必须先创建外部类的对象,才能创建内部类的对象。非静态成员内部类是属于某个外部类对象的
  • 更多特征
    • 非静态内部类不能有静态方法、静态属性和静态初始化块
    • 外部类的静态方法、静态代码块不能访问非静态内部类,包括不能使用非静态内部类定义变量、创建实例

注意:只要编译成功,内部类和外部类就会成为两个完全不同的类。Outer外部类和Inner内部类编译完成后是Outer.class和Outer$Inner.class两个类的字节码文件。所以内部类是相对独立的存在,其成员变量/方法名可以和外部类相同

11.2. 静态成员内部类

public class OuterClass1 {
    private static String name;
    private static int num = 0;
    public OuterClass1() {}
    public OuterClass1(String name, int num) {
        this.name = name;
        this.num = num;
    }
    public static void methodOut() {
        System.out.println("method out");
        InnerClass1 ic = new InnerClass1();
        InnerClass1.methodInner1();
    }
    public void methodOut2() {
        // 外部类不可以直接访问内部的成员变量和成员方法
        InnerClass1 ic = new InnerClass1();
        System.out.println(ic.num); // 20
        ic.methodInner();
        InnerClass1.methodInner1();
    }
    static class InnerClass1 {
        private String type;
        private int num = 20;
        public InnerClass1() {
        }
        public InnerClass1(String type, int num) {
            this.type = type;
            this.num = num;
        }
        public void methodInner() {
            // 静态内部类只能访问外部类的静态成员
            System.out.println(name); // null
            int num = 30;
            System.out.println(num); // 30
            System.out.println(this.num); // 20
            // 静态内部类访问外部类的同名的成员变量
            System.out.println(OuterClass1.num); // 0
            methodOut();
        }
        public static void methodInner1() {
        }
    }
}
import com.wyb.neibulei.OuterClass1.InnerClass1;
public class Test1 {
    public static void main(String[] args) {
        // 要创建静态成员内部类对象,不需要先创建外部类的对象
        OuterClass1.InnerClass1 ic = new OuterClass1.InnerClass1();
        ic.methodInner();
        InnerClass1 ic1 = new InnerClass1();
        ic1.methodInner();
    }
}
  • 总结
    • 静态内部类只能够访问外部类的静态成员
    • 静态内部类访问外部类的同名成员变量:OuterClass.num
    • 静态内部类属于整个外部类。创建静态内部类对象,不需要先创建外部类的对象
    • 外部类可以通过类名直接访问内部类的静态成员,访问非静态成员依旧需要先创建内部类对象

11.3. 局部内部类

定义在方法内部,作用域只限于本方法,称为局部内部类
局部内部类用来解决比较复杂的问题,想创建一个类来辅助我们的解决方案,又不希望这个类是公用的,就需要使用局部内部类。局部内部类和成员内部类一样被编译,只是他的作用域发生了改变,只能在该方法中被使用。
局部内部类在实际开发中应用很少

public class OuterClass2 {
    int num = 10;
    private void method() {
        int num1 = 20;
        class InnerClass {
            public void method1() {
                num = 100;
                System.out.println(num1);
                // num1 = 200;
            }
        }
        InnerClass ic = new InnerClass();
        ic.method1();
    }
}

注意:局部内部类访问所在方法的局部变量,要求局部变量必须使用final修饰。JSK1.8中final可以省略,编译后会加final

11.4. 匿名内部类

匿名内部类是一种特殊的局部内部类
前提:存在一个类或接口,这里的类可以是具体类也可以是抽象类
本质是一个继承了该类或实现了该接口的子类匿名对象
适合只需要创建一次对象的类

  • 语法:
new 父类构造器(实参列表) 实现接口() {
	// 匿名内部类类体
}
public interface Comparator {
    public int compare(Object obj1, Object obj2);
}
public class BookNameComparator implements Comparator {
    @Override
    public int compare(Object obj1, Object obj2) {
        Book book1 = (Book)obj1;
        Book book2 = (Book)obj2;
        return book1.getBookName().compareTo(book2.getBookName());
    }
}
public class BookPriceNameComparator implements Comparator {
    @Override
    public int compare(Object obj1, Object obj2) {
        Book book1 = (Book)obj1;
        Book book2 = (Book)obj2;
        if(book1.getPrice() > book2.getPrice()){
            return -1;
        }else if(book1.getPrice()< book2.getPrice()){
            return 1;
        }else{
            return book1.getBookName().compareTo(book2.getBookName());
        }
    }
}

如果某个外部比较器只使用一次或者很少的次数,就可以不提供专门的类,而是使用匿名内部类。

public class Test2 {
    public static void main(String[] args) {
        Comparable comp;
        Comparator comp2;
        Book book1
                = new Book("倚天屠龙记1","金庸1","清华大学出版社",35);
        Book book2
                = new Book("倚天屠龙记5","金庸5","清华大学出版社",35);
        int result = book1.compareTo(book2);
        System.out.println(result);

        Comparator cmp1 = new BookNameComparator();
        result = cmp1.compare(book1,book2);
        System.out.println(result);
        Comparator cmp2 = new BookPriceNameComparator();
        result = cmp2.compare(book1,book2);
        System.out.println(result);

        Comparator cmp3 = new Comparator() {
            //代码块,每次创建对象的时候执行,并且早于构造方法执行
            {
                System.out.println("--匿名内部类通过代码块完成初始化操作---");
            }
            @Override
            public int compare(Object obj1, Object obj2) {
                Book book1 = (Book)obj1;
                Book book2 = (Book)obj2;
                return book1.getAuthor().compareTo(book2.getAuthor());
            }
        };
        result = cmp3.compare(book1,book2);
        System.out.println(result);
    }
}
  • 结论
    • 匿名内部类可以实现一个接口,也可以继承一个类(可以是抽象类)
    • 匿名内部类只能实现一个接口,而不是多个
    • 必须实现所有的方法,匿名内部类不能是抽象类
    • 匿名内部类不可能有构造方法,因为类是匿名的
    • 匿名内部类没有访问修饰符
    • 如果想实现构造方法的一些初始化功能,通过代码块实现
    • 如果要访问所在方法的局部变量,该变量需要使用final修饰(JDK1.8可省略final)
  • 内部类的作用
    • 内部类提供了更小的封装。只能让外部类直接访问,不允许同一个包中的其他类直接访问
    • 内部类可以直接访问外部类的私有属性,内部类被当成其外部类的成员。但外部类不能访问内部类的内部属性
    • 接口只是解决了多重继承的部分问题,而内部类使得多重继承的解决方案变得更加完整
    • 用匿名内部类实现回调功能。
  • 内部类的使用场合
    • 在只为外部类提供服务的情况下,优先考虑使用内部类
    • 使用内部类间接实现多继承:每个内部类独立的继承一个类或实现某些接口,无论外部类是否已经继承了某个类或实现了某些接口,对于内部类没有影响
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Qi妙代码

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

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

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

打赏作者

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

抵扣说明:

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

余额充值