Java基础学习——第六章 面向对象编程(下)

Java基础学习——第六章 面向对象编程(下)

一、关键词:static

1. static关键字的引入

  • 当我们编写一个类时,其实就是在描述其对象的属性和行为,而并没有产生实质上的对象,只有通过new关键字才会实例化对象,这时系统才会分配内存空间给对象,其方法才可以供外部调用
  • 但有时也希望无论是否产生了对象或无论产生了多少对象,某些特定的结构(属性、 方法、 代码块、 内部类)在内存空间里只有一份,即所有类的对象都共享这些结构,不必在每一个实例对象中单独分配**==
  • 此时就需要引入static关键字。static修饰的类成员不归具体的某个对象所有,而是被所有类的对象所共享

2. static关键字的使用

  1. static表示静态的,可以用来修饰类的成员:属性、 方法、 代码块、 内部类不能用来修饰构造器!
  2. static修饰的成员具备以下特点:
    • 随着类的加载而加载,且只加载一次
    • 优先于对象存在(不依赖于类的对象)
    • static修饰的成员,被所有类的对象所共享
    • 在访问权限允许的条件下,static修饰的成员不需要实例化对象,即可直接被类调用(通过类名来访问)
2.1 static修饰属性(静态变量、类变量)
  1. 属性(成员变量),按是否使用static修饰,可以分为:类变量(静态变量) vs 实例变量(非静态变量)
    • 实例变量(非静态变量):类的每个对象都拥有一套独立的非静态属性,存储在不同的内存空间中,不能被类的不同对象所共享。当修改其中一个对象的非静态属性时,不会影响其他对象中的非静态属性
    • 类变量(静态变量):类的所有对象都共享同一个静态属性。当通过某一个对象或直接通过类名去访问并修改该静态属性时,再通过其他对象访问该静态属性,其值也发生了变化
类变量(静态变量)实例变量(非静态变量)
通过==类名==访问yesno
通过==对象==访问yesyes
  1. 类变量(静态变量)随着类的加载而加载,优先于对象存在。由于类只加载一次,所以静态变量在内存中(方法区的静态域)也只会存在一份,被所有类的对象所共享。可以通过 “类名.静态变量” 的方式访问,当然也可以通过对象来访问静态变量
  2. 类变量 vs 实例变量内存解析:
public class Day14_StaticTest {
    public static void main(String[] args) {
        //通过类名来访问静态属性
        Chinese.nation = "中国";
        Chinese c1 = new Chinese();
        //通过类的对象来访问非静态属性
        c1.name = "姚明";
        c1.age = 40;
        //通过类的对象来访问静态属性
        c1.nation = "CHN";
        Chinese c2 = new Chinese();
        c2.name = "马龙";
        c2.age = 30;
        c2.nation = "China";
        
        //Chinese.age = 32 //无法通过类名来访问非静态变量,因为每个对象都拥有独立的非静态变量,不能共享
    }
}

class Chinese {
    String name;
    int age;
    static String nation; //静态属性
}

在这里插入图片描述

2.2 static修饰方法(静态方法、类方法)
  1. 类方法(静态方法)随着类的加载而加载,可以通过 “类名.静态方法” 的方式调用,也可以通过对象来调用
类方法(静态方法)实例方法(非静态方法)
通过==类名==调用yesno
通过==对象==调用yesyes
public class Day14_StaticTest {
    public static void main(String[] args) {
        //通过类名来调用静态方法
        Chinese.show();
        Chinese c1 = new Chinese();
        //通过类的对象调用静态方法
        c1.show();
        //通过类的对象调用非静态方法
        c1.eat();
        //无法通过类名来调用非静态方法
        Chinese.eat();
    }
}

class Chinese {
    //非静态方法
    public void eat() {
    	System.out.println("吃")
    }
    //静态方法
    public static void show() {
    	System.out.println("中国")
    }
}
  1. **静态方法中,只能调用当前类的static方法和属性(相当于前面省略了一个类名),不能调用非static的方法和属性(生命周期不够);**非静态方法中,既可以调用static的方法和属性,也可以调用非static的方法和属性
  2. 在静态方法内,不能使用this、super关键字。因为this表示当前类的对象,super表示父类的对象,而静态方法是不依赖于类的对象的(不需要实例化对象就可以调用)
2.3 如何判断属性或方法是否要声明为static?
  1. 可以被同一类的不同对象所共享,不会随着对象的不同而改变的属性,通常声明为static
  2. 类中的常量,如Math.PI,也通常声明为static
  3. 操作static属性的方法(如getter setter 方法),通常声明为static的
  4. Math、Arrays、Collections等工具类中的方法,通常声明为static的,可以直接通过类名来调用

3. static练习

例1 Circle类
public class CircleTest {
    public static void main(String[] args) {
        Circle c1 = new Circle();
        Circle c2 = new Circle();
        System.out.println(c1.getId()); //1001
        System.out.println(c2.getId()); //1002
        System.out.println(Circle.getTotal()); //2  static方法直接通过类名来调用
    }
}

class Circle {
	private double radius; //半径
	private int id; //编号
	private static int total; //创建的Circle对象的个数:static
	private static int init = 1001; //static属性被所有对象共享
	
    //构造器
    public Circle() {
        id = init++; //Circle对象的编号id自动增加
        total++; //创建的Circle对象的个数自动增加
    }
    
    public Circle(double radius) {
        this(); //调用了重载的空参构造器,相当于替换了下面两行
        //id = init++; //Circle对象的编号id自动增加
        //total++; //创建的Circle对象的个数自动增加
		this.radius = radius;
    }
    
    public double getRadius() {
		return radius;
	}
    
    public void setRadius(double radius) {
		this.radius = radius;
	}
    
    public int getId() {
		return id;
	}
    
    //static属性的getter、setter方法也会自动声明为static的
    public static int getTotal() {
        return total;
    }
    
    public double findArea() {
		return Math.PI * radius * radius;
	}
}
例2 银行账户

编写一个银行账户类,属性有“帐号”、“密码”、“存款余额”、“利率”、“最小余额”,定义封装这些属性的方法。 账号要自动生成。编写测试类,使用银行账户类,输入、输出储户的上述信息。考虑:哪些属性可以声明为static?

public class Day14_AccountTest {
    public static void main(String[] args) {
        Account2 acct1 = new Account2();
        Account2 acct2 = new Account2("123456", 2000);
        System.out.println(acct1); //自动调用了重写的toString方法
        System.out.println(acct2);
    }
}

class Account2 {
    private int id; //id
    private String pwd;
    private double balance;
    private static double interestRate;
    private static double minBalance = 1.0;
    private static int init;

    //构造器
    public Account2() {
        id = init++; //id自动生成
    }

    public Account2(String pwd, double balance) {
        this(); //调用重载的空参构造器
        this.pwd = pwd;
        this.balance = balance;
    }

    public int getId() {
        return id;
    }

    public String getPwd() {
        return pwd;
    }

    public void setPwd(String pwd) {
        this.pwd = pwd;
    }

    public double getBalance() {
        return balance;
    }

    public static double getInterestRate() {
        return interestRate;
    }

    public static void setInterestRate(double interestRate) {
        //静态方法中不能使用this,interestRate为静态属性,直接用类名调用
        Account2.interestRate = interestRate;
    }

    public static double getMinBalance() {
        return minBalance;
    }

    public static void setMinBalance(double minBalance) {
        Account2.minBalance = minBalance;
    }

    @Override
    public String toString() {
        return "Account2{" +
                "id=" + id +
                ", pwd='" + pwd + '\'' +
                ", balance=" + balance +
                '}';
    }
}

4. 单例设计模式

4.1 设计模式
  1. 设计模式是在大量的实践中总结和理论化之后优选的代码结构、编程风格、以及解决问题的思考方式。 设计模免去我们自己再思考和摸索。
  2. 23种设计模式:
    • 创建型模式(5种):工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式
    • 结构型模式(7种):适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式
    • 行为型模式(11种):策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式
4.2 类的单例设计模式
  1. 对某个类,只允许在类的内部存在一个对象实例;且该类只提供一个公共的静态的方法,返回唯一的对象实例
  2. 实现步骤:
    • 将类的构造器声明为private,避免在类的外部调用构造器创建(new)该类的对象
    • 在类的内部调用该私有构造器创建该类的对象,也声明为private
    • 由于在类的外部无法创建类的对象,只能调用该类的某个==公共的静态方法以返回类内部创建的对象;==静态方法中只能访问类中的静态成员变量,因此,指向类内部对象实例的引用类型变量也必须定义成静态的
  3. 实现方式:饿汉式 vs 懒汉式
4.3 单例模式的饿汉式实现
public class Day14_SingletonTest1 {
    public static void main(String[] args) {
        //在类的外部调用该类的公共的静态方法,返回类内部创建的唯一的对象
        SingletonBank bank1 = SingletonBank.getInstance();
        SingletonBank bank2 = SingletonBank.getInstance();
        System.out.println(bank1 == bank2); //true 二者共同指向类内部创建的唯一的对象
    }
}

//饿汉式1:一般的写法
class SingletonBank {
    //1.类的构造器声明为private,避免在类的外部调用构造器new新的对象
    private SingletonBank() {
    }
    
    //2.在类的内部调用该私有构造器创建该类的对象,也声明为private(饿汉式在调用方法前就把对象new好了)
    //4.由于静态方法中只能访问类中的静态成员变量,因此,指向类内部对象实例的引用类型变量也必须定义成静态的
    private static SingletonBank instance = new SingletonBank();
    
    //3.提供一个公共的静态的方法,返回类内部创建的唯一的对象实例
    public static SingletonBank getInstance() {
        return instance;
    }
}

//饿汉式2:使用静态代码块
class SingletonBank {
    //1.类的构造器声明为private,避免在类的外部调用构造器new新的对象
    private SingletonBank() {
    }
    
    //2.在类的内部声明一个当前类对象的引用(private),然后在静态代码块中为其new一个类的对象
    //4.由于静态方法中只能访问类中的静态成员变量,因此,指向类内部对象实例的引用类型变量也必须定义成静态的
    private static SingletonBank instance = null;
    
    //由于静态代码块随着类的加载而执行,在方法调用前对象就存在了,因为该写法是饿汉式
    static{
        instance = new SingletonOrder();
    }
    
    //3.提供一个公共的静态的方法,返回类内部创建的唯一的对象实例
    public static SingletonBank getInstance() {
        return instance;
    }
}
4.4 单例模式的懒汉式实现
public class Day14_SingletonTest2 {
    public static void main(String[] args) {
        SingletonOrder order1 = SingletonOrder.getInstance();
        SingletonOrder order2 = SingletonOrder.getInstance();
        System.out.println(order1 == order2);
    }
}

class SingletonOrder {
    //1.类的构造器声明为private,避免在类的外部调用构造器new新的对象
    private SingletonOrder() {
    }

    //2.在类的内部声明一个当前类对象的引用(private),但不对其进行初始化(懒汉式只有在调用方法后才new对象)
    //4.由于静态方法中只能访问类中的静态成员变量,因此,指向类内部对象实例的引用类型变量也必须定义成静态的
    private static SingletonOrder instance = null;

    //3.提供一个公共的静态的方法,返回类内部创建的唯一的对象实例
    public static SingletonOrder getInstance() {
        if (instance == null) {
            //与饿汉式不同,懒汉式直接在方法内部创建对象,且只创建一次
            instance = new SingletonOrder();
        }
        return instance;
    }
}
4.5 饿汉式 vs 懒汉式
饿汉式懒汉式
优点线程安全的只有在调用静态方法后才new对象,延迟了对象的创建
缺点在调用静态方法前就把对象new好了,对象加载时间(生命周期)过长暂时还存在线程安全问题,讲到多线程时,可修复
4.6 单例设计模式的应用场景
  1. 单例模式的优点:由于单例模式只生成一个对象实例, 减少了系统性能开销。当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决。如:java.lang.Runtime类
  2. 应用场景:
    • 网站的计数器,一般也是单例模式实现,否则难以同步
    • 应用程序的日志应用,一般都使用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作, 否则内容不好追加
    • 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源
    • 读取配置文件的类,一般也只有一个对象。没有必要每次使用配置文件数据,都生成一个对象去读取
    • Application也是单例的典型应用
    • Windows的**Task Manager(任务管理器)**是典型的单例模式
    • Windows的**Recycle Bin(回收站)**是典型的单例应用。系统运行过程中, 回收站一直维护着仅有的一个实例

二、main()方法的使用说明

  • main()方法是程序的入口,JVM运行程序的时候首先找的就是main方法,main()方法的格式是固定的:
public static void main(String[] args){}
  1. public: 表示程序的访问权限,表示的是任何的场合可以被引用,即其他类都可以访问这个main函数
  2. static: 告知编译器main方法是一个静态方法,main方法中的代码是存储在静态存储区的,即当定义了类以后这段代码就已经存在了。如果main()方法没有使用static修饰符,那么编译不会出错,但是如果试图执行该程序将会报错,提示main()方法不存在。因为包含main()的类并没有实例化(即没有这个类的对象),所以其main()方法也不会存在。而使用static修饰符则表示该方法是静态的,不需要实例化即可使用,即当Java虚拟机(JVM)加载类时,就会执行该方法。
    static 修饰符能够与变量、方法一起使用,表示是"静态"的,不依赖类的对象的静态变量和静态方法能够通过类名来访问,不需要实例化一个类的对象来访问该类的静态成员,所以static修饰的成员又称作类变量和类方法。静态变量与实例变量不同,实例变量总是通过对象来访问,因为它们的值在对象和对象之间有所不同。
  3. void: 表明main()的返回值是无类型的,即main方法是不需要返回值的
  4. String[] args: 字符串数组,接收来自程序执行时传进来的参数。如果是在控制台,可以通过编译执行将参数传进来,命令行如下:
    javac HelloWorld.java
    java HelloWorld a b c //字符串以空格的方式隔开
    这样传进main函数的就是一个字符串数组,args[0]=a;args[1]=b;args[2]=c, 如果不传参数进来,args为空。
  • 由于Java虚拟机需要调用类的main()方法,所以该方法的访问权限必须是public, 又因为Java虚拟机在执行main()方法时不必创建对象,所以该方法必须是static的,该方法接收一个String类型的数组参数,该数组中保存执行Java命令时传递给所运行的类的参数。
  • 又因为main() 方法是静态的,我们不能直接访问该类中的非静态成员,必须创建该类的一个实例对象后,才能通过这个对象去访问类中的非静态成员,这种情况,我们在之前的例子中多次碰到。

三、类的成员之四:代码块

1. 代码块(初始化块)

  1. 代码块的作用:对Java类或对象进行初始化
  2. 代码块只能被static修饰, 称为静态代码块(static block), 没有使用static修饰的, 为非静态代码块
  3. 代码块的分类:静态代码块 vs 分静态代码块

2. 静态代码块

  1. 可以包含输出语句
  2. 静态代码块==随着类的加载而执行,且只执行一次==
  3. 通常用于初始化类的属性,即static的属性
  4. 静态代码块中==只能调用静态的属性和方法,不能调用非静态的属性和方法==,即无法对非静态的属性进行初始化
  5. 若一个类中定义了多个静态代码块,则根据声明的先后顺序依次执行
  6. 静态代码块的执行要优先于非静态代码块的执行

3. 非静态代码块

  1. 可以包含输出语句
  2. 非静态代码块==随着对象的创建而执行;每创建一个对象,都会执行一次。 且先于构造器执行==
  3. 可以在创建对象时,对对象的属性进行初始化
  4. 非静态代码块**既可以调用非静态的属性和方法, 还可以调用静态的属性和方法**
  5. 若一个类中定义了多个非静态代码块,则根据声明的先后顺序依次执行
public class Day14_BlockTest {
    public static void main(String[] args) {
        //静态代码块随着类的加载而执行,且只执行一次
        String desc = BlockPerson.desc; //static block
        System.out.println(desc);

        //非静态代码块随着对象的创建而执行;每创建对象一个对象,都会执行一次
        BlockPerson p1 = new BlockPerson(); //block
        BlockPerson p2 = new BlockPerson(); //block
    }
}

class BlockPerson {
    //属性
    String name;
    int age;
    static String desc = "人";

    //构造器
    public BlockPerson() {
    }

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

    //静态代码块
    static {
        System.out.println("static block");
        desc = "好人";
    }

    //非静态代码块
    {
        System.out.println("block");
    }

    //方法
    public void eat() {
        System.out.println("吃饭");
    }

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

4. 体会代码块和构造器的加载顺序:由父及子,静态先行

例1
class Root{
    static{
        System.out.println("Root的静态初始化块"); //1
    }
    {
        System.out.println("Root的普通初始化块"); //4
    }
    public Root(){
        //隐藏了一个super();
        System.out.println("Root的无参数的构造器"); //5
    }
}
class Mid extends Root{
    static{
        System.out.println("Mid的静态初始化块"); //2
    }
    {
        System.out.println("Mid的普通初始化块"); //6
    }
    public Mid(){
        //隐藏了一个super();
        System.out.println("Mid的无参数的构造器"); //7
    }
    public Mid(String msg){
        //通过this调用同一类中重载的构造器
        this();
        System.out.println("Mid的带参数构造器,其参数值:" //8
                + msg);
    }
}
class Leaf extends Mid{
    static{
        System.out.println("Leaf的静态初始化块"); //3
    }
    {
        System.out.println("Leaf的普通初始化块"); //9
    }
    public Leaf(){
        //通过super调用父类中有一个字符串参数的构造器
        super("尚硅谷");
        System.out.println("Leaf的构造器"); //10
    }
}
public class LeafTest{
    public static void main(String[] args){
        new Leaf();
        //new Leaf(); //如果再加上这一句,静态代码块就不用再加载了,其他的顺序不变
    }
}
例2
//由父及子 静态先行
class Father {
    static {
        System.out.println("11111111111"); //1
    }

    {
        System.out.println("22222222222"); //4
    }

    public Father() {
        System.out.println("33333333333"); //5
    }

}

public class Son extends Father {
    static {
        System.out.println("44444444444"); //2
    }

    {
        System.out.println("55555555555"); //6
    }

    public Son() {
        //super();
        System.out.println("66666666666"); //7
    }


    public static void main(String[] args) { //先加载类(由父及子 静态先行),再执行main方法,
        System.out.println("77777777777"); //3
        System.out.println("************************");
        new Son();
        System.out.println("************************");

        new Son();
        System.out.println("************************");
        new Father();
    }
}

5. 属性赋值的先后顺序(总结)

  1. 默认初始化
  2. 显式初始化(在声明属性时即赋一个初始化值)和代码块初始化,同级别下按声明先后顺序依次执行
  3. 构造器中初始化
  4. 通过"对象.方法"和"对象.属性"的方式赋值
//显式初始化和多个代码块,同级别下按声明先后顺序依次执行
public class OrderTest {
	public static void main(String[] args) {
        Order1 order1 = new Order1();
        System.out.println(order1.orderId); //4
        Order2 order2 = new Order2();
        System.out.println(order2.orderId); //3
    }
}

class Order1 {
    int order = 3;
    {
        order = 4;
    }
}

class Order2 {
    {
        order = 4;
    }
    int order = 3;
}

四、关键字:final

1. final关键字的使用

  1. 在Java中声明==类、 变量(包括成员变量和局部变量)和方法==时, 可使用关键字final来修饰,表示“最终的”
  2. ==final修饰的类不能被继承。==提高安全性, 提高程序的可读性。如:String、 System、 StringBuffer类
  3. ==final修饰的方法不能被子类重写。==如: Object类中的getClass()方法
  4. final修饰的变量(属性或局部变量),即称为常量。 名称大写, 且只能被赋值一次。
    • final修饰的属性(成员变量)必须==在声明时显式初始化或在每个代码块构造器==中显式赋值, 然后才能使用
    • final修饰局部变量,尤其是修饰方法的形参时,表明该形参是一个常量。当调用此方法时,给常量形参赋一个实参。一旦赋值以后,就只能在方法体内调用该形参,而不能再对其重新赋值
  5. static final 可以用来修饰属性和方法:
    • 用来修饰属性:一般称为全局常量,随着类的加载而加载,可以直接通过类名调用,且是一个常量
    • 用来修饰方法:随着类的加载而加载,可以直接通过类名调用,且不能被子类重写

2. final关键字【面试题】:改错

例1
  • final修饰的形参一旦赋值以后,就只能调用,而不能再对其重新赋值
public class Something {
	public int addOne(final int x) { 
		return ++x; //不合法:final修饰的形参一旦赋值以后,就只能调用,而不能再对其重新赋值
		// return x + 1; //合法
	}
}
例2
  • 这里final修饰的形参o是一个引用类型变量,对其进行赋值以后,只要保证其保存的地址值不变即可,即只要保证不再让他指向别的对象即可。而 o.i++; 只是改变了其指向对象的属性值,并不会影响o的值,因此是合法的。而 o = new Other(); 显然是不合法的
public class Something {
	public static void main(String[] args) {
		Other o = new Other();
		new Something().addOne(o);
	}
    
	public void addOne(final Other o) {
        o.i++; //合法
        // o = new Other(); //不合法
	}
}

class Other {
	public int i;
} 

五、关键字:abstract(抽象类与抽象方法)

1. abstract抽象概念的引入

  • 随着继承层次中一个个新子类的定义,类变得越来越具体,而父类则更一般,更通用。类的设计应该保证父类和子类能够共享特征。有时将一个父类设计得非常抽象,以至于它没有具体的实例,这样的类叫做抽象类
  • 抽象类是用来模型化那些父类无法确定全部实现,而是由其子类提供具体实现的对象的类。换句话来说就是,抽象类只是提供一个大致的框架,而具体的实现方法由其子类提供。

2. abstract关键字的使用

  1. abstract表示抽象的,可以用来修饰:类、方法
  2. 不能用abstract修饰变量(属性和局部变量)、代码块、构造器
  3. 不能用abstract修饰 private方法(不能被重写)、static方法(不能被重写)、 final方法(不能被重写)、 final类(不能被继承)
2.1 abstract修饰类:抽象类
  1. 抽象类不能被实例化,即不能创建对象
  2. 抽象类中==一定有构造器,供子类对象实例化时调用==(子类对象实例化的全过程:一定会调用父类的构造器)
  3. 抽象类是用来被继承的。在开发中,都会提供抽象类的子类,让其子类实例化对象,完成相关操作
2.2 abstract修饰方法:抽象方法
  1. 抽象方法:只有方法的声明,没有具体的方法体,以分号结束
public abstract void talk();
  1. 包含抽象方法的类,一定是抽象类;反之,抽象类中可以没有抽象方法
  2. ① 抽象类的==子类必须重写父类(包括直接父类和所有间接父类)中所有的抽象方法==,并提供方法体,此时该子类才能实例化;② 若抽象类的子类没有重写父类(包括直接父类和所有间接父类)中所有的抽象方法,该子类仍为抽象类,必须用abstract修饰

3. 创建抽象类的匿名子类的对象

public class PersonTest {
	public static void main(String[] args) {
		Worker worker = new Worker();
        //1. 抽象类的非匿名子类的非匿名对象
        method(worker);
        //2. 抽象类的非匿名子类的匿名对象
        method(new Worker());
        
        //3. 抽象类的匿名子类的非匿名对象,该匿名子类的对象又通过多态的方式赋给了父类的引用
        //注意:匿名子类的内部({}内)需要对抽象类中的所有抽象方法进行重写
        Person p = new Person() {
            @Override
            public void eat() {
            }

            @Override
            public void breath() {
            }
        };
        
        method(p);
        
        //4. 抽象类的匿名子类的匿名对象
        method(new Person() {
            @Override
            public void eat() {
            }

            @Override
            public void breath() {
            }
        });
	}
    
    public static void method(Person p) {
        p.eat();
        p.breath();
    }
}

//抽象类
abstract class Person {
    //抽象方法
	public abstract void eat();
    public abstract void breath();
}

//抽象类的子类
class Worker extends Person {
    @Override
    public void eat() {
    }
    
    @Override
    public void breath() {
    }
}

4. 模板方法设计模式(抽象类、多态性)

  1. 抽象类体现的就是一种模板模式的设计,抽象类作为多个子类的通用模板,子类在抽象类的基础上进行扩展、改造,但子类总体上会保留抽象类的行为方式
  2. 解决的问题:
  • 当功能内部一部分实现是确定的, 一部分实现是不确定的,可以把不确定的部分暴露出去,让子类去实现
  • 换句话说,在软件开发中实现一个算法时,整体步骤很固定、通用,这些步骤已经在父类中写好了。但是某些部分易变,易变部分可以抽象出来,供不同子类实现。这就是一种模板模式

5. abstract练习题

编写工资系统, 实现不同类型员工(多态)的按月发放工资。 如果当月出现某个Employee对象的生日, 则将该雇员的工资增加100元。

  1. 定义一个Employee类,该类包含:
    private成员变量name,number,birthday,其中birthday 为MyDate类的对象;
    abstract方法earnings();
    toString()方法输出对象的name,number和birthday
  2. MyDate类包含:
    private成员变量year,month,day ;
    toDateString()方法返回日期对应的字符串: xxxx年xx月xx日
  3. 定义SalariedEmployee类继承Employee类,实现按月计算工资的员工处理。该类包括: private成员变量monthlySalary;
    实现父类的抽象方法earnings(),该方法返回monthlySalary值; toString()方法输出员工类型信息及员工的name,number,birthday
  4. 参照SalariedEmployee类定义HourlyEmployee类,实现按小时计算工资的员工处理。该类包括:
    private成员变量wage和hour;
    实现父类的抽象方法earnings(),该方法返回wage和hour值;
    toString()方法输出员工类型信息及员工的name,number,birthday。
  5. 定义PayrollSystem类,创建Employee变量数组并初始化,该数组存放各类雇员对象的引用。利用循环结构遍历数组元素,输出各个对象的类型,name,number,birthday,以及该对象生日。当键盘输入本月月份值时,如果本月是某个Employee对象的生日,还要输出增加工资信息
import java.util.Scanner;

public class PayrollSystem {
    public static void main(String[] args) {
        Scanner scan = new Scanner(System.in);
        System.out.print("请输入当前月份:");
        int month = scan.nextInt();
        
        //虽然Employee是抽象类,不能实例化对象,但是这里只是创建了一个Employee类型的对象数组,
        //数组的元素是Employee类型的引用,可以通过对象的多态性指向子类的对象
        Employee[] emps = new Employee[2];
        emps[0] = new SalariedEmployee("马森", 1002, new MyDate(1991, 1, 27), 10000); 
        emps[1] = new HourlyEmployee("潘雨生", 2001, new MyDate(1998, 9, 12), 60, 240);
        
        for (int i = 0; i < emps.length; i++) {
            System.out.println(emps[i]); //自动调用了子类重写的toString方法
            //抽象类的多态情况,编译时调用的是抽象父类中的抽象方法,运行时实际执行的是子类重写父类的方法
            System.out.println("月工资" + emps[i].earnings());
            if (month == emps[i].getBirthday().getMonth()) {
                System.out.println("增加工资");
            }
        }
    }
}

abstract class Employee {
    private String name;
    private int number;
    private MyDate birthday;

    public Employee(String name, int number, MyDate birthday) {
        this.name = name;
        this.number = number;
        this.birthday = birthday;
    }
    
    public MyDate getBirthday() {
        return birthday;
    }
    
    //抽象方法
    public abstract double earnings();

    @Override
    public String toString() {
        //由于没有在MyDate类中重写toString方法,这里要调用toDateString()方法才能正确的输出年月日信息
        return "name='" + name + '\'' + ", number=" + number + ", birthday=" + birthday.toDateString();
    }
}

class MyDate {
    private int year;
    private int month;
    private int day;

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

    public int getMonth() {
        return month;
    }

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

class SalariedEmployee extends Employee {
    private double monthlySalary;

    public SalariedEmployee(String name, int number, MyDate birthday, double monthlySalary) {
        super(name, number, birthday);
        this.monthlySalary = monthlySalary;
    }

    @Override
    public double earnings() {
        return monthlySalary;
    }

    @Override
    public String toString() {
        return "SalariedEmployee{" + super.toString() + '}'; //直接调用父类中的toString方法
    }
}

class HourlyEmployee extends Employee {
    private double wage;
    private int hour;

    public HourlyEmployee(String name, int number, MyDate birthday, double wage, int hour) {
        super(name, number, birthday);
        this.wage = wage;
        this.hour = hour;
    }

    @Override
    public double earnings() {
        return wage * hour;
    }

    @Override
    public String toString() {
        return "HourlyEmployee{" + super.toString() + '}'; //直接调用父类中的toString方法
    }
}

六、关键字:interface(接口)

1. 接口的概述

  1. 一方面, 有时必须从几个类中派生出一个子类, 继承它们所有的属性和方法。 但是, Java不支持多重继承。 有了接口, 就可以得到“多重继承”的效果,即一个类可以实现多个接口
  2. 另一方面, 有时必须从几个类中抽取出一些共同的行为特征,而它们之间又没有is-a的关系,仅仅是具有相同的行为特征或功能而已
  3. 接口就是规范,定义的是一组规则,体现了现实世界中“如果你是/要…则必须能…”的思想。继承是一个"是不是"的关系,而接口实现则是 "能不能"的关系
  4. 接口的本质是契约,标准,规范,就像我们的法律一样。制定好后大家都要遵守
  5. 面向接口编程!

2. 接口的定义和使用

  1. 接口使用==interface==来定义,而类使用class来定义。接口和类是Java中两个并列的结构
  2. 接口的定义:定义接口中的成员
    • JDK7及以前:接口中只能定义==全局常量抽象方法==
      • 全局常量:接口中的所有属性(成员变量)都默认声明为 public static final,可以省略不写
      • 抽象方法:接口中的所有方法都默认声明为 public abstract,可以省略不写
    • JDK8新特性:接口中除了定义全局常量和抽象方法外,还可以定义静态方法、默认方法
  3. 接口中==不能定义构造器,即接口不可以实例化对象==
  4. 接口的主要用途:通过类(实现类)来实现(implements)接口(面向接口编程)
  5. ① 接口的==实现类必须实现接口中所有的抽象方法==,并提供方法体,此时该实现类才能实例化;② 若接口的实现类没有实现接口中所有的抽象方法,则该实现类仍为抽象类,必须用abstract修饰
  6. 一个类可以实现多个接口——弥补了Java单继承性的局限性(类:单继承,多实现
    • 类的声明格式: ① 先写extends,后写implements;② 如果该类实现了多个接口,接口之间用逗号隔开
class AA extends BB implements CC, DD, EE {}
  1. 接口与接口之间可以继承,而且可以多继承(接口:多继承
interface AA extends BB, CC, DD {}
  1. 与继承关系类似,接口与实现类之间存在多态性
//类
public class Day15_InterfaceTest {
    public static void main(String[] args) {
        System.out.println(Flyable.MAX_SPEED);
        System.out.println(Flyable.MIN_SPEED);

        Plane plane = new Plane();
        plane.fly();
        plane.stop();
    }
}

//接口
interface Flyable {
    //全局常量
    public static final int MAX_SPEED = 7900;
    int MIN_SPEED = 1; //省略了public static final

    //抽象方法
    public abstract void fly();

    void stop(); //省略了public abstract
}

interface Attackable {
    void attack();
}

//使用实现类来实现接口
//实现类必须实现接口中所有的抽象方法,此时该实现类才能实例化
class Plane implements Flyable {

    @Override
    public void fly() {
        System.out.println("起飞");
    }

    @Override
    public void stop() {
        System.out.println("停止");
    }
}

//若实现类没有实现接口中所有的抽象方法,则该实现类仍为抽象类,必须用abstract修饰
abstract class Kite implements Flyable {
    @Override
    public void fly() {
    }
}

//一个类可以实现多个接口
class Bullet extends Object implements Flyable, Attackable, CC {
    @Override
    public void fly() {
    }

    @Override
    public void stop() {
    }

    @Override
    public void attack() {
    }

    @Override
    public void method1() {
    }

    @Override
    public void method2() {
    }
}

//*************** 接口的多继承 ***************
interface AA {
    void method1();
}

interface BB {
    void method2();
}

interface CC extends AA, BB {

}

3. JDK8对接口的改进(新特性)

  • 接口中除了可以定义全局常量和抽象方法外,还可以定义静态方法和默认方法
3.1. 接口中的静态方法
  1. 使用==static==关键字修饰,包含方法体
  2. 接口中声明的静态方法,只能通过接口名调用,不能通过实现类的对象调用
    • 而类中声明的静态方法,可以通过类名调用,可以通过类的对象调用;可以通过子类名调用,也可以通过子类的对象调用
3.2. 接口中的默认方法
  1. 使用==default==关键字修饰。
  2. 接口中声明的默认方法,可以==通过实现类对象来调用==
  3. 接口中声明的默认方法,可以在实现类中重新实现(重写)。如果实现类重写了接口中的默认方法,则实现类对象实际调用的是重写后的方法
  4. 如果子类(或实现类)继承的父类和实现的接口中声明了同名同参数的方法(接口中是默认方法),那么在子类(或实现类)没有重写该方法的情况下,默认调用的是父类中同名同参数的方法——类优先原则(只针对方法)
  5. 如果实现类实现了多个接口,而多个接口中恰好定义了同名同参数的默认方法,那么在实现类没有重写该方法的情况下,编译会报错——接口冲突,此时必须在实现类中重写该方法
  6. 如何在子类(或实现类)的方法中调用父类(或接口中)被重写的方法:
    • 调用父类中被重写的方法:super.方法名()
    • 调用接口中被重写的默认方法:接口名.super.方法名()
public class Day15_CompareA {
    public static void main(String[] args) {
        //接口中声明的静态方法,只能通过接口名调用,不能通过实现类的对象调用
        CompareA.method1();
        //new SubClass().method1(); //编译错误:接口中声明的静态方法,不能通过实现类的对象调用

        //类中声明的静态方法,可以通过类名调用,可以通过类的对象调用;可以通过子类名调用,也可以通过子类的对象调用
        AAA.method(); //1
        new AAA().method(); //1
        BBB.method(); //1
        new BBB().method(); //1

        //接口中的默认方法,可以通过实现类的对象来调用。而且可以在实现类中重新实现(重写)
        new SubClass().method2();
        new SubClass().method3();
    }
}

interface CompareA {
    //JDK8新特性1:接口中的静态方法,可以直接通过接口名调用静态方法
    public static void method1() {
        System.out.println("CompareA:北京");
    }

    //JDK8新特性2:接口中的默认方法,使用default关键字修饰,可以通过实现类的对象来调用
    public default void method2() {
        System.out.println("CompareA:上海");
    }

    //public 可以省略
    default void method3() {
        System.out.println("CompareA:上海");
    }
}

class SubClass extends SuperClass implements CompareA {
    @Override
    public void method2() {

    }

    @Override
    public void method3() {

    }

    public void myMethod() {
        //调用子类(或实现类)重写的方法
        method3();
        //调用父类中的方法
        super.method3();
        //调用接口中的默认方法(非静态)
        CompareA.super.method3();
    }
}

class SuperClass {
    public void method3() {

    }
}

class AAA {
    public static void method() {
        System.out.println("1");
    }
}

class BBB extends AAA {

}
3.3. 接口冲突的解决 & 类优先原则
interface Filial {// 孝顺的
	default void help() {
		System.out.println("老妈,我来救你了!");
	}
}

interface Spoony {// 痴情的
	default void help() {
		System.out.println("媳妇, 别怕,我来了!");
	}
}

class Father {
    public void help() {
        System.out.println("儿子,救我媳妇!");
    }
}

//情况1:接口冲突————实现类实现了多个接口,而多个接口中恰好定义了同名同参数的默认方法
class Man implements Filial, Spoony {
    //对多个接口中同名同参数的方法进行重写
	@Override
	public void help() {
		System.out.println("我该怎么办呢? ");
        //调用指定接口中被重写的方法:接口名.super.方法名()
		Filial.super.help();
		Spoony.super.help();
	}
}

//情况2:子类(或实现类)继承的父类和实现的接口中声明了同名同参数的方法(接口中是默认方法)
class Man extends Father implements Filial, Spoony {
    //那么在子类(或实现类)没有重写该方法的情况下,默认调用的是父类中同名同参数的方法——类优先原则(只针对方法)
	}
}

4. 接口的多态性

由于接口不能实例化对象,接口的使用需要多态性。具体表现为==接口和实现类之间的多态性==

  • 对象的多态性:父类的引用指向了子类的对象。编译时调用的是父类中的方法,运行时实际执行的是子类重写父类的方法
  • 接口的多态性:接口的引用指向了实现类的对象。编译时调用的是接口中的抽象方法,运行时实际执行的是实现类重新实现的方法
public class Day15_USBTest {
    public static void main(String[] args) {
        Computer c1 = new Computer();
        //接口的多态性:接口的引用指向了实现类的对象;
        //编译时调用的是接口中的抽象方法,运行时实际执行的是实现类重新实现的方法
        c1.transferData(new Flash());
        c1.transferData(new Printer());
    }
}

class Computer {
    //形参为接口的引用类型变量,由于接口无法实例化对象,体现了多态性
    public void transferData(USB usb) {
        usb.start();
        usb.stop();
    }
}

//接口体现了一种规范
interface USB {
    void start();
    void stop();
}

//实现类:必须对接口中的所有抽象方法进行重新实现才能实例化对象(不声明为abstract)
class Flash implements USB {
    @Override
    public void start() {
    }

    @Override
    public void stop() {
    }
}

//实现类
class Printer implements USB {
    @Override
    public void start() {
    }

    @Override
    public void stop() {
    }
}

5. 创建接口的匿名实现类的对象

  • 与==创建抽象类的匿名子类的对象==形式类似
public class Day15_USBTest {
    public static void main(String[] args) {
        Computer c1 = new Computer();
        Flash flash = new Flash();
        //1. 接口的非匿名实现类的非匿名对象
        c1.transferData(flash);
        
        //2. 接口的非匿名实现类的匿名对象
        c1.transferData(new Flash());
        
        //3. 接口的匿名实现类的非匿名对象,该匿名实现类的对象又通过多态的方式赋给了接口的引用
        //注意:匿名实现类的内部({}内)需要对接口中的所有抽象方法进行重新实现
        USB phone = new USB() {
            @Override
    		public void start() {
    		}

    		@Override
    		public void stop() {
    		}
        };
        
        c1.transferData(phone);
        
        //接口的匿名实现类的匿名对象
        c1.transferData(new USB() {
            @Override
    		public void start() {
    		}

    		@Override
    		public void stop() {
    		}
        });
    }
}

class Computer {
    public void transferData(USB usb) {
        usb.start();
        usb.stop();
    }
}

//接口
interface USB {
    void start();
    void stop();
}

//实现类
class Flash implements USB {
    @Override
    public void start() {
    }

    @Override
    public void stop() {
    }
}

6. 接口的应用

6.1 代理模式

代理模式是Java开发中使用较多的一种设计模式, 代理设计就是为其他对象提供一种代理以控制对这个对象的访问

public class Day15_NetWorkTest {
    public static void main(String[] args) {
        ProxyServer proxyServer = new ProxyServer(new Server()); //多态,接口的引用指向了实现类的对象
        proxyServer.browse();
    }
}

interface NetWork {
    public abstract void browse();
}

//被代理类
class Server implements NetWork {

    @Override
    public void browse() {
        System.out.println("真实的服务器访问网络");
    }
}

//代理类
class ProxyServer implements NetWork {
    private NetWork work; //代理类中声明一个接口的引用,用在多态情况下指向被代理类的对象

    public ProxyServer(NetWork work) {
        this.work = work;
    }

    public void check() {
        System.out.println("联网之前的检查工作");
    }

    @Override
    public void browse() {
        check();
        work.browse(); //虚拟方法调用
    }
}
6.2 工厂模式(工厂方法模式和抽象工厂模式)

7. 接口练习

例1:排错

父类和接口中声明了同名属性x,由于类和接口是并列的结构,导致编译器无法确定具体调用的是谁的属性

  • 如果调用的是父类的属性,则声明为 super.x
  • 如果调用的是接口的属性,则声明为 A.x
interface A {
    int x = 0; //全局常量,前面省略了 public static final
}

class B {
    int x = 1;
}

class C extends B implements A {
    public void pX() {
        //Reference to 'x' is ambiguous, both 'B.x' and 'A.x' match
        //System.out.println(x); //编译错误
        System.out.println(super.x); //调用父类的属性
        System.out.println(A.x); //调用接口的属性,静态属性可以直接通过类名来访问
    }

    public static void main(String[] args) {
        new C().pX();
    }
}
例2:排错
  • 接口中的属性是全局常量,前面省略了 public static final,无法被重新赋值
interface Playable {
	void play();
}

interface Bounceable {
	void play();
}

interface Rollable extends Playable, Bounceable {
	Ball ball = new Ball("PingPang"); //全局常量,前面省略了 public static final,无法被重新赋值
}

class Ball implements Rollable {
	private String name;
	
    public String getName() {
		return name;
	}

    public Ball(String name) {
		this.name = name;
	}

    public void play() {
		ball = new Ball("Football"); //接口中的属性是全局常量,final的常量不能被重新赋值
		System.out.println(ball.getName());
	}
}
例3 定义一个接口实现两个对象的比较
  1. 定义一个接口用来实现两个对象的比较:
interface CompareObject{
	public int compareTo(Object o); //若返回值是0,代表相等;正数代表当前对象大;负数代表当前对象小
}
  1. 定义一个Circle类,声明radius属性,提供getter和setter方法
  2. 定义一个ComparableCircle类,继承Circle类并且实现CompareObject接口。在ComparableCircle类中给出接口中方法compareTo的实现体,用来比较两个圆的半径大小
  3. 定义一个测试类InterfaceTest, 创建两个ComparableCircle对象,调用compareTo方法比较两个类的半径大小
  4. 思考:参照上述做法定义矩形类 Rectangle 和 ComparableRectangle 类,在ComparableRectangle类中给出compareTo方法的实现,比较两个矩形的面积大小。
public class InterfaceTest {
    public static void main(String[] args) {
        ComparableCircle c1 = new ComparableCircle(3.4);
        ComparableCircle c2 = new ComparableCircle(3.6);
        System.out.println(c1.compareTo(c2)); //-1
    }
}

interface CompareObject {
    public int compareTo(Object o); //若返回值是0,代表相等;正数代表当前对象大;负数代表当前对象小
}

class Circle3 {
    private double radius;

    public Circle3() {
    }

    public Circle3(double radius) {
        this.radius = radius;
    }

    public double getRadius() {
        return radius;
    }

    public void setRadius(double radius) {
        this.radius = radius;
    }
}

class ComparableCircle extends Circle3 implements CompareObject {

    public ComparableCircle(double radius) {
        super(radius);
    }

    //类似于equals()方法的重写
    @Override
    public int compareTo(Object o) {
        if (this == o) {
            return 0;
        }
        if (o instanceof CompareObject) {
            ComparableCircle c = (ComparableCircle) o;
            if (this.getRadius() > c.getRadius()) {
                return 1;
            } else if (this.getRadius() == c.getRadius()) {
                return 0;
            } else {
                return -1;
            }
        }
        throw new RuntimeException("传入的数据类型不匹配");
    }
}
例4:抽象类与接口的异同?
  • 共同点:
    1. 不能实例化对象,在使用时一般都用到了对象的多态性
    2. 抽象类和接口内都可以定义抽象方法(抽象类不一定包含抽象方法,接口中通常都会包含抽象方法)
  • 不同点:
    1. 抽象类可以包含构造器、抽象方法、普通方法、属性;而接口只能包含全局常量和抽象方法,JDK8.0新增了静态方法和默认方法,JDK9.0新增了私有方法。抽象类可以定义构造器,而接口中不能定义构造器
    2. 抽象类的使用一般通过子类来继承抽象类,而接口的使用一般通过实现类来实现接口
    3. 一个子类只能继承一个抽象类;而一个接口可以继承多个接口(类的单继承性,接口的多继承性)
    4. 二者的关系:抽象类可以实现多个接口(类与接口的多实现性)
    5. 抽象类常见设计模式是模板方法模式;而接口常见设计模式是代理模式、工厂方法模式和抽象工厂模式
    6. 抽象类实际上是作为一个模板;而接口实际是一种标准、规范,表示一种能力

七、类的成员之五:内部类

1. 内部类的引入

  1. Java中允许将一个类A声明在另一个类B的内部,前者(类A)称为内部类,后者(类B)称为外部类
  2. 内部类的分类:成员内部类 vs 局部内部类
    • 成员内部类:直接声明在类的内部,又可以分为静态(static)成员内部类和非静态(非static)成员内部类
    • 局部内部类:声明在方法体内、代码块内或构造器内

2. 成员内部类

  1. 一方面,作为外部类的成员:
    • 外部类只能被2种权限修饰符(public和缺省)修饰,而成员内部类==可以被4种权限修饰符修饰==
    • 成员内部类能直接==调用外部类的所有成员,包括private的结构==
    • 成员内部类==可以声明为static==,但此时不能再调用外部类非static的结构,只能调用外部类static的结构
    • 非static成员内部类中的成员不能声明为static的,只有在外部类或static成员内部类中才可声明static成员
    • 当想要在外部类的static成员部分使用内部类时, 可以考虑内部类声明为static的
    • 外部类访问成员内部类的成员,需要通过 “内部类.成员” 或 “内部类对象.成员” 的方式
  2. 另一方面,作为一个类:
    • 可以在类内定义属性、方法、构造器等结构
    • 可以声明为final的,表示该内部类不能被继承
    • 可以声明为abstract的,表示该内部类不能实例化对象,但可以被其它的内部类继承
    • 编译以后生成OuterClass$InnerClass.class字节码文件(也适用于局部内部类)
2.1 成员内部类的使用

关注两个问题:

  1. 如何实例化成员内部类的对象
  2. 如何在成员内部类中区分调用外部类的结构
public class Day15_InnerClassTest {
    public static void main(String[] args) {
        //*************** 如何实例化成员内部类的对象 ***************
        //实例化静态成员内部类的对象
        Person3.Dog dog = new Person3.Dog();
        dog.show();

        //实例化非静态成员内部类的对象
        //Person3.Bird bird = new Person3.Bird(); //错误的
        Person3 p = new Person3();
        Person3.Bird bird = p.new Bird();
        bird.sing();
    }
}

class Person3 {
    String name;
    int age;

    public void eat() {
        System.out.println("人:吃饭");
    }

    //静态成员内部类
    static class Dog {
        String name;
        int age;

        public void show() {
            System.out.println("卡拉是条狗");
            //eat(); //静态成员内部类中不能调用外部类非静态的结构
        }
    }

    //非静态成员内部类
    class Bird {
        String name;

        public Bird() {
        }

        public void sing() {
            System.out.println("鸟");
            eat(); //前面实际上省略了: 外部类名.this.
            Person3.this.eat(); //如何在成员内部类中调用外部类中的结构
        }

        //*************** 如何在成员内部类中区分调用外部类的结构 ***************
        public void display(String name) {
            System.out.println(name); //方法的形参
            System.out.println(this.name); //内部类的属性:this表示当前类的对象
            System.out.println(Person3.this.name); //外部类的属性
        }
    }
}

3. 局部内部类

  • 局部内部类:声明在方法体、代码块或构造器内部
public class Day15_InnerClassTest1 {
    //开发中不常用
    public void method() {
        //局部内部类
        class AA {

        }
    }

    //返回了一个Comparable接口的实现类的对象(多态)
    public Comparable getComparable1() {
        //创建一个Comparable接口的实现类:局部内部类
        class MyComparable implements Comparable {
            @Override
            public int compareTo(Object o) {
                return 0;
            }
        }
        return new MyComparable();
    }

    public Comparable getComparable2() {
        //匿名实现类的对象
        return new Comparable() {
            @Override
            public int compareTo(Object o) {
                return 0;
            }
        };
    }
}
 {
            System.out.println("卡拉是条狗");
            //eat(); //静态成员内部类中不能调用外部类非静态的结构
        }
    }

    //非静态成员内部类
    class Bird {
        String name;

        public Bird() {
        }

        public void sing() {
            System.out.println("鸟");
            eat(); //前面实际上省略了: 外部类名.this.
            Person3.this.eat(); //如何在成员内部类中调用外部类中的结构
        }

        //*************** 如何在成员内部类中区分调用外部类的结构 ***************
        public void display(String name) {
            System.out.println(name); //方法的形参
            System.out.println(this.name); //内部类的属性:this表示当前类的对象
            System.out.println(Person3.this.name); //外部类的属性
        }
    }
}
  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值