Java编程思想读书笔记——第七章:复用类

第七章 复用类

使用类而不破坏现有程序代码,有两种达到这一目的的方法:

  • 在新的类中产生现有类的对象,也就是说new一个对象,这种方法称为组合
  • 按照现有类的类型来创建新类,不改变现有类的形式,在其基础上添加新代码,这种方法称为继承

7.1 组合语法

每一个非基本类型的对象都有一个方法:toString(),重写这个方法可以返回一个String对象

null对象调用方法会出现运行时错误,也就是我们常说的空指针,但是可以打印一个null引用

初始化引用的四种方式:

1、在定义对象的地方,在构造器被调用之前初始化

2、在类的构造器中

3、在正要使用这些对象之前,称为惰性初始化,可以减少额外的负担,

4、使用实例初始化

练习1:创建一个简单的类,在第二个类中,将第一个引用定义为第一个类的对象,惰性初始化

7.2 继承语法

如果子类重写了父类的某个方法,那么子类的对象调用的将是子类中重写的方法

练习2、从Detergent中继承产生一个新的类。覆盖scrub()并添加一个名为sterilize()的新方法

7.2.1 初始化基类

初始化类的时候,是从基类往外扩散的,也就是说先执行基类的构造器,再执行子类的构造器,即使你不为此类创建构造器,编译器也会默认为你合成一个构造器,并调用父类的构造器

练习3、证明前面这句话

class CartoonWithDefCtor extends Drawing { 
//!  CartoonWithDefCtor() { 
//!      System.out.println("CartoonWithDefCtor constructor"); 
//!  } 
} 
 
public class E03_CartoonWithDefCtor {   
    public static void main(String args[]) {     
        new CartoonWithDefCtor ();   
    }
}
// 运行结果
Art constructor 
Drawing constructor 

练习4、证明基类构造器:a、总是会被调用,b、在导出类构造器之前被调用

class Base1 {   
    public Base1() { 
        System.out.println("Base1"); 
    } 
} 
 
class Derived1 extends Base1 {   
    public Derived1() { 
        System.out.println("Derived1"); 
    } 
} 
 
class Derived2 extends Derived1 {   
    public Derived2() { 
        System.out.println("Derived2"); 
    } 
} 
 
public class E04_ConstructorOrder {   
    public static void main(String args[]) {     
        new Derived2();   
    } 
}
// 运行结果 
Base1 
Derived1 
Derived

练习5、创建两个带默认构造器的类A和类B,从A中继承产生一个名为C的新类,并在C内创建一个B类的成员,不要给C编写构造器,创建一个C类的对象

class A {   
    public A() { 
        System.out.println("A()"); 
    } 
} 
 
class B {  
    public B() { 
        System.out.println("B()");
    } 
} 
 
class C extends A {   
    B b = new B(); 
} 
 
public class E05_SimpleInheritance {  
    public static void main(String args[]) {     
        new C();  
    } 
}
// 运行结果
A()
B()
 

带参数的构造器

如果没有默认的构造器,或者想调用一个带参数的基类构造器,必须用关键字super

否则会报错

练习6、用Chess.java来证明前一段话

class ChessWithoutDefCtor extends BoardGame {           
//    ChessWithoutDefCtor () {   
//        System.out.println("ChessWithoutDefCtor constructor");   
//        super(11);   
//    } 
} 
 
public class E06_ChessWithoutDefCtor {   
    public static void main(String args[]) {     
        new ChessWithoutDefCtor();   
    } 
}
// 直接报错 

练习7、修改练习5,使A和B以带参数的构造器取代默认的构造器,为C写一个构造器,并在其中执行所有的初始化

class A2 {   
    public A2(String s) { 
        System.out.println("A2(): " + s); 
    } 
} 
 
class B2 {   
    public B2(String s) { 
        System.out.println("2B(): " + s); 
    } 
} 
 
class C2 extends A2 {   
    B2 b;   
    public C2(String s) {     
        super(s);     
        b = new B2(s);   
    } 
} 
 
public class E07_SimpleInheritance2 {   
    public static void main(String args[]) {     
        new C2("Init string");   
    } 
}
// 运行结果
A2()
2B()

练习8、创建一个基类,它仅有一个非默认构造器,再创建一个导出类,他带有默认构造器和非默认构造器,在导出类的构造器中调用基类的构造器

class BaseNonDefault {   
    public BaseNonDefault(int i) {
    } 
} 
 
class DerivedTwoConstructors extends BaseNonDefault {   
    public DerivedTwoConstructors() { 
        super(47); 
    } 
 
    public DerivedTwoConstructors(int i) { 
        super(i); 
    } 
} 
 
public class E08_CallBaseConstructor {   
    public static void main(String args[]) {     
        new DerivedTwoConstructors();     
        new DerivedTwoConstructors(74);   
    } 
}

练习9、创建一个root类,含有名为Component1、2、3类的的各一个实例,root中派生一个类Stem,也含有上述组成部分

class Component1 {   
    public Component1() { 
        System.out.println("Component1"); 
    } 
} 
 
class Component2 {   
    public Component2() { 
        System.out.println("Component2"); 
    } 
} 
 
class Component3 {   
    public Component3() { 
        System.out.println("Component3"); 
    } 
} 
 
class Root {   
    Component1 c1 = new Component1();   
    Component2 c2 = new Component2();   
    Component3 c3 = new Component3();   
    public Root() { 
        System.out.println("Root"); 
    } 
} 
 
class Stem extends Root {   
    Component1 c1 = new Component1();   
    Component2 c2 = new Component2();   
    Component3 c3 = new Component3(); 
    public Stem() { 
        System.out.println("Stem"); 
    } 
} 
 
public class E09_ConstructorOrder2 {   
    public static void main(String args[]) {     
        new Stem();   
    } 
}
// 运行结果
Component1 
Component2 
Component3 
Root 
Component1 
Component2 
Component3 
Stem 

先初始化父类的成员,然后执行父类构造器
之后初始化子类的成员,然后执行子类构造器

练习10、修改练习9,使每个类都仅具有非默认的构造器

class Component1b {   
    public Component1b(int i) {     
        System.out.println("Component1b " + i);   
    } 
} 
 
class Component2b {   
    public Component2b(int i) {     
        System.out.println("Component2b " + i);   
    } 
} 
 
class Component3b {   
    public Component3b(int i) {     
        System.out.println("Component3b " + i);   
    } 
} 
 
class Rootb {   
    Component1b c1 = new Component1b(1);   
    Component2b c2 = new Component2b(2);   
    Component3b c3 = new Component3b(3);   
    public Rootb(int i) { 
        System.out.println("Rootb"); 
    } 
} 
 
class Stemb extends Rootb {   
    Component1b c1 = new Component1b(4);   
    Component2b c2 = new Component2b(5);   
    Component3b c3 = new Component3b(6);   
    public Stemb(int i) {     
        super(i);     
        System.out.println("Stemb");   
    } 
} 
 
public class E10_ConstructorOrder3 {   
    public static void main(String args[]) {     
        new Stemb(47);   
    } 
}
// 运行结果
Component1b 1 
Component2b 2 
Component3b 3 
Rootb 
Component1b 4 
Component2b 5 
Component3b 6 
Stemb 

附加:继承的执行顺序


// 狗1
public class Dog1 {
    public Dog1(String s) { 
        System.out.println("Dog1" + s); 
    } 
}

// 狗2
public class Dog2 {
	public Dog2(String s) { 
        System.out.println("Dog2" + s); 
    } 
}

// 父类
public class Father {
	static Dog1 c1 = new Dog1("父类静态成员");   
	Dog2 c2 = new Dog2("父类成员");
   
	static {
		System.out.println("父类静态代码块");
	}
	
	{
		System.out.println("父类非静态代码块");
	}

	public Father() { 
		System.out.println("父类无参数构造方法"); 
	}

	public Father(String s) {
		System.out.println("父类有参构造方法" + s);
	}
}

// 子类
public class Son extends Father {
	static Dog1 c1 = new Dog1("子类静态成员");   
	Dog2 c2 = new Dog2("子类成员");   

	static {
		System.out.println("子类静态代码块");
	}
	
	{
		System.out.println("子类非静态代码块");
	}

	public Son() { 
		System.out.println("子类无参数构造方法"); 
	}

	public Son(String s) {
        super(s);
		System.out.println("子类有参构造方法" + s);
	}
}

// 运行
public class Test {  

    public static void main(String args[]) { 
    	new Son();
    } 
}  

// 运行结果

Dog1父类静态成员
父类静态代码块
Dog1子类静态成员
子类静态代码块
Dog2父类成员
父类非静态代码块
父类无参数构造方法
Dog2子类成员
子类非静态代码块
子类无参数构造方法

// 如果改为new Son("1"),运行结果

Dog1父类静态成员
父类静态代码块
Dog1子类静态成员
子类静态代码块
Dog2父类成员
父类非静态代码块
父类有参构造方法1
Dog2子类成员
子类非静态代码块
子类有参构造方法1

// 如果把Son类有参构造方法中的super注释掉,运行结果
// 区别在与父类不执行有参构造方法,而执行默认的构造方法

Dog1父类静态成员
父类静态代码块
Dog1子类静态成员
子类静态代码块
Dog2父类成员
父类非静态代码块
父类无参数构造方法
Dog2子类成员
子类非静态代码块
子类有参构造方法1

先不说继承,就是一个类正常的初始化:

  1. 肯定是先静态(静态成员和静态代码块谁在前谁先执行),将变量和代码块都看作是成员,同级的
  2. 然后非静态(非静态成员和非静态代码块也是谁在前谁先执行),将变量和代码块都看作是成员,同级的
  3. 最后是构造方法

继承的顺序是这样的:

继承的时候,记住一点,静态优先,所有就有了:

  1. 父类的静态成员,父类的静态代码块
  2. 子类的静态成员,子类的静态代码块
  3. 父类的成员,父类的非静态代码块,父类的构造方法
  4. 子类的成员,子类的非静态代码块,子类的构造方法
  5. 子类的有参构造方法没有super父类的构造方法,那么子类执行有参构造方法会默认调用父类的无参构造方法

7.3 代理

Java语言并没有提供对代理的直接支持,代理是继承和组合的中庸之道

  • 继承是直接使用把父类方法占为己有
  • 组合是new一个对象,然后去调用
  • 代理是new一个对象,然后在类中的方法调用这个对象的方法,相当于就多了一层

练习11、修改Detergent.java,让它使用代理

class DetergentDelegation {   
    private Cleanser cleanser = new Cleanser();   
    // Delegated methods:   
    public void append(String a) { 
        cleanser.append(a); 
    } 
    public void dilute() { 
        cleanser.dilute(); 
    }   
    public void apply() { 
        cleanser.apply(); 
    }   
    public String toString() { 
        return cleanser.toString(); 
    }   
    public void scrub() {     
        append(" DetergentDelegation.scrub()");     
        cleanser.scrub();   
    }   
    public void foam() { 
        append(" foam()"); 
    }   
    public static void main(String[] args) {     
        DetergentDelegation x = new DetergentDelegation();     
        x.dilute();     
        x.apply();     
        x.scrub();     
        x.foam();     
        print(x);     
        print("Testing base class:");     
        Cleanser.main(args);   
    } 
} 
 
public class E11_Delegation {   
    public static void main(String[] args) {             
        DetergentDelegation.main(args);   
    } 
}

7.4 结合使用组合和继承

这个其实就是你在继承的同时,又new了其他类的对象

7.4.1 确保正确清理

try...finally

练习12、将一个适当的dispose()方法的层次结构添加到练习9的所有类中

class Component1c {   
    public Component1c(int i) {     
        System.out.println("Component1c");   
    }   
    public void dispose() {     
        System.out.println("Component1c dispose"); 
    } 
} 
 
class Component2c {   
    public Component2c(int i) {     
        System.out.println("Component2c");   
    }       
    public void dispose() {     
        System.out.println("Component2c dispose");   
    } 
} 
 
class Component3c {   
    public Component3c(int i) {     
        System.out.println("Component3c");   
    }   
    public void dispose() {     
        System.out.println("Component3c dispose");   
    } 
} 
 
class Rootc {   
    Component1c c1 = new Component1c(1);   
    Component2c c2 = new Component2c(2);  
    Component3c c3 = new Component3c(3);   
    public Rootc(int i) { 
        System.out.println("Rootc"); 
    }   
    public void dispose() {     
        System.out.println("Rootc dispose");     
        c3.dispose();     
        c2.dispose();     
        c1.dispose();   
    } 
} 
 
class Stemc extends Rootc {   
    Component1c c1 = new Component1c(4);   
    Component2c c2 = new Component2c(5);   
    Component3c c3 = new Component3c(6);   
    public Stemc(int i) {     
        super(i);     
        System.out.println("Stemc");   
    }   
    public void dispose() {     
        System.out.println("Stemc dispose");     
        c3.dispose();     
        c2.dispose();     
        c1.dispose(); 
        super.dispose();   
    } 
} 
 
public class E12_Dispose {   
    public static void main(String args[]) {     
        new Stemc(47).dispose();   
    } 
} 

// 运行结果
Component1c 
Component2c 
Component3c 
Rootc 
Component1c 
Component2c 
Component3c 
Stemc 
Stemc dispose 
Component3c dispose 
Component2c dispose 
Component1c dispose 
Rootc dispose 
Component3c dispose 
Component2c dispose 
Component1c dispose 


// 从这个练习要注意到一点,重写方法后,如果不super,就不会调用父类的方法
 

7.4.2 名称屏蔽

  • 在Java中,如果去重载父类的方法,是允许的,父类的方法并不会被覆盖
  • Java SE5新增了@Override注解,很明确的告诉你哪些方法是重写的,一目了然
  • 并且可以防止你在不想重载时而意外地进行了重载

练习13、创建一个类,它应带有一个被重载了三次的方法,继承产生一个新类,并添加一个该方法的新的重载定义,展示这四个方法在到处类中都是可以使用的

class ThreeOverloads {   
    public void f(int i) {     
        System.out.println("f(int i)");   
    } 
    public void f(char c) {     
        System.out.println("f(char c)");   
    }   
    public void f(double d) {     
        System.out.println("f(double d)");   
    } 
} 
 
class MoreOverloads extends ThreeOverloads {   
    public void f(String s) {     
        System.out.println("f(String s)");   
    } 
} 
 
public class E13_InheritedOverloading {   
    public static void main(String args[]) {     
        MoreOverloads mo = new MoreOverloads();     
        mo.f(1);     
        mo.f('c');     
        mo.f(1.1);     
        mo.f("Hello");   
    } 
}
 
// 运行结果
f(int i) 
f(char c) 
f(double d) 
f(String s) 

7.5 在组合与继承之间的选择

  • 组合是显示的,继承是隐式的
  • is-a的关系是用继承来表达的
  • has-a的关系是则是用组合来表达的

练习14、在Car.java中给Engine添加一个service()方法,并在main()中调用该方法

class ServicableEngine extends Engine {   
    public void service() {} 
} 
 
class ServicableCar {   
    public ServicableEngine engine = new ServicableEngine();   
    public Wheel[] wheel = new Wheel[4];   
    public Door left = new Door(), right = new Door(); 
    // 2-door   
    public ServicableCar() {     
        for(int i = 0; i < 4; i++)       
        wheel[i] = new Wheel();   
    } 
} 
 
public class E14_ServicableEngine {   
    public static void main(String[] args) {     
        ServicableCar car = new ServicableCar();             
        car.left.window.rollup();     
        car.wheel[0].inflate(72);     
        car.engine.service();   
    } 
}

7.6 protected关键字

就类用户而言,这是private的,但对于任何继承于此类的到处类或其他任何位于用一个包内的类来说,它却是可以访问的

练习15、在包中编写一个类,类应具备一个protected方法,在包外部去调用该protected方法并解释其结果,然后,从你的类中继承产生一个类,并从导出类的方法内部调用该protected方法

public class E15_Protected {   
    protected void f() {} 
} 


package reusing; 
import reusing.protect.*; 
 
class Derived extends E15_Protected {   
    public void g() {     
        f(); 
    // Accessible in derived class   
    } 
} 
 
public class E15_ProtectedTest {   
    public static void main(String args[]) { 
    //! new E15_Protected().f(); 
    // Cannot access     
    new Derived().g();   
    } 
}

7.7 向上转型

  • 简单理解意思就是子类也是属于父类的
  • 汽车属于交通工具
  • 苹果属于水果

public void getInfo(Fruits fruits) {
    XXX
}

public static void main(String[] args) {
    Apple apple = new Apple();
    getInfo(apple);
}

// 这种操作也是可以的,方法中参数是Fruits,但是传入Apple对象也是没问题的

7.7.1 为什么称之为向上转型

因为在继承图上是子类用一个空箭头指向了父类

导出类是基类的一个超集,可能比基类含有更多的方法

7.7.2 再论组合和继承

其实继承技术是不太常用的

用组合还是用继承,问一问自己:是否需要从新类向基类进行向上转型

练习16、创建一个名为Amphibian的类,由此继承产生一个称为Frog的类,在基类中设置适当的方法,向上转型,并说明所有方法都可工作

class Amphibian {   
    public void moveInWater() {     
        System.out.println("Moving in Water");   
    }   
    public void moveOnLand() {     
        System.out.println("Moving on Land");   
    } 
} 
 
class Frog extends Amphibian {} 
 
public class E16_Frog {   
    public static void main(String args[]) {     
        Amphibian a = new Frog();     
        a.moveInWater();     
        a.moveOnLand();   
    } 
}
// 运行结果
Moving in Water 
Moving on Land

练习17、修改练习16,使Frog覆盖基类中方法的定义,请留心main()中都发生了什么

class Frog2 extends Amphibian {   
    public void moveInWater() {     
        System.out.println("Frog swimming");   
    }   
    public void moveOnLand() {     
        System.out.println("Frog jumping");   
    } 
} 
 
public class E17_Frog2 {   
    public static void main(String args[]) {     
        Amphibian a = new Frog2();     
        a.moveInWater();     
        a.moveOnLand();   
    } 
} 
// 运行结果
Frog swimming 
Frog jumping

// 这个很明确了,重写父类方法后,调用的是重写后的方法,父类方法就不调用了
// 还记得之前写程序时,基础不扎实,会这方面的困惑,想着是不是父类和子类的对象都会调用啊,额哈哈

7.8 final 关键字

  • final意为无法改变的,不想改变会出于两种理由:设计或效率

7.8.1 final 数据

有时数据的恒定不变是很有用的,比如:

  1. 一个永不改变的编译时常量
  2. 一个在运行时被初始化的值,而你不希望它被改变
  • 编译期常量这种情况,必须是基本数据类型,并且以final关键字表示,在定义的时候必须赋值
  • 对于基本类型使用final,使数值恒定不变
  • 对于对象引用使用final,使引用恒定不变,初始化指向一个对象就无法指向另一个对象,但是对象自身是可以修改的,Java并没有提供使任何对象恒定不变的途径(可以自己去实现),注意数组也是对象

public static final是一种典型的对常量进行定义的方式

public表示可以被用于包外,static强调只有一份,final说明它是一个常量

练习18、创建一个含有static final域和final域的类,说明二者间的区别

class SelfCounter {   
    private static int count; 
    private int id = count++;   
    public String toString() { 
        return "SelfCounter " + id; 
    } 
} 
 
class WithFinalFields {   
    final SelfCounter scf = new SelfCounter();   
    static final SelfCounter scsf = new SelfCounter();   
    public String toString() {     
        return "scf = " + scf + "\nscsf = " + scsf;   
    } 
} 
 
public class E18_FinalFields {   
    public static void main(String args[]) {             
        System.out.println("First object:");     
        System.out.println(new WithFinalFields());             
        System.out.println("Second object:");     
        System.out.println(new WithFinalFields());   
    } 
} 
// 运行结果
First object: 
scf = SelfCounter 1 
scsf = SelfCounter 0 
Second object: 
scf = SelfCounter 2 
scsf = SelfCounter 0 

// 注意先初始化static修饰的对象

空白final

  • Java允许空白final,无论如何都要确保空白final在使用前被初始化,在定义处或者构造器中初始化
  • 我们可以这样做:在全局定义空白final,但是在多个不同的构造方法中对被final修饰的值进行初始化

练习19、创建一个含有指向某对象的空白final引用的类,在所有构造器内部都执行空白final的初始化动作

class WithBlankFinalField { 
    private final Integer i;   
    // Without this constructor, you'll get a compiler error:   
    // "variable i might not have been initialized"   public         
    WithBlankFinalField(int ii) {     
        i = new Integer(ii);    
    }   
    public Integer geti() {     
    // This won't compile. The error is:     
    // "cannot assign a value to final variable i"     
    // if(i == null)     
    //   i = new Integer(47);     
        return i;   
    } 
} 
 
public class E19_BlankFinalField {   
    public static void main(String args[]) {     
        WithBlankFinalField wbff = new WithBlankFinalField(10);             
        System.out.println(wbff.geti());   
    } 
}

// 运行结果
10

final 参数

  • 意味着你无法在方法中更改参数引用所指向的对象
  • 这一特性主要用来向匿名内部类传递数据

7.8.2 final方法

使用final方法的原因有两个:

  1. 把方法锁定,以防止任何继承类修改它的含义
  2. 效率,之前可以消除方法调用的开销,但是现在的Java版本已经不再需要使用final方法来进行优化了,可以说,现在使用final不会给性能带来任何的提升

final和private关键字

  • 类中所有的private方法都隐式地指定为是final的,给private方法加final没有任何的意义
  • 如果去重写private方法,并没有覆盖,而是生成了一个新的方法

练习20、展示@Override注解可以解决本节中的问题

  • 开发中常用的,没啥说的,略了

练习21、创建一个带final方法的类,由此继承产生一个类并尝试覆盖该方法

  • 无法覆盖,被final修饰的方法无法被重写,略了

7.8.3 final类

类定义为final时,表示你打算继承这个类了,也不允许别人这样做,不希望它有子类

final类中所有的方法都隐式指定为是final的,因为无法覆盖它们

练习22、创建一个final类并试着继承它

  • 肯定无法继承,没啥说的,略了

7.8.4 有关final的忠告

设计类时,要考虑到别人可能想要复用你的类,所以注意是否要使用final

  • Vector中大部分方法都被指定为final,这导致任何人无法复用和优化它,被ArrayList取代
  • Hashtable不含任何final方法,没太读懂啥意思,可能就是一个final方法也不含,很多人修改了代码,导致设计糟糕?被HashMap取代

7.9 初始化及类的加载

  • 每个类的编译代码都存在于它自己的独立文件中,该文件只在需要使用的时候才会被加载
  • 一般来说类的代码在初次使用的时候才加载,创建类的第一个对象时,或者访问static域或static方法时
  • 构造器也是static方法,尽管并没有显式的写出来,准确的讲,类是在任何static成员被访问时加载的

7.9.1 继承与初始化

拿main方法执行的代码来说一下执行的顺序:

  • 执行Beetle.main(),记载器自动找出Beetle类的编译代码(.class文件中)
  • 执行的时候发现Beetle有一个基类,就去加载基类,如果还有第二个基类,再去加载第二个基类
  • 最最的基类中的static域会初始化,接着按顺序初始化static域
  • 必要的类都加载完毕,对象可以被创建了,基本类型被赋默认值,对象引用设为null
  • 基类的变量初始化,基类的构造器被调用,接着按顺序初始化

练习23、请证明加载类的动作仅发生一次,证明该类的第一个实体的创建或者对static成员的访问都有可能引起加载

class LoadTest {   
// The static clause is executed   
// upon class loading:   
    static {     
        System.out.println("Loading LoadTest");   
    }   
    static void staticMember() {} 
} 
 
public class E23_ClassLoading {   
    public static void main(String args[]) {             
        System.out.println("Calling static member");             
        LoadTest.staticMember();     
        System.out.println("Creating an object");     
        new LoadTest();   
    } 
}

// 运行结果 
Calling static member 
Loading LoadTest 
Creating an object

练习24、在Beetle.java中,从Beetle类继承产生一个具体类型的甲壳虫

class JapaneseBeetle extends Beetle {   
    int m = printInit("JapaneseBeetle.m initialized");       
    JapaneseBeetle() {     
        print("m = " + m);     
        print("j = " + j);   
    }   
    static int x3 = printInit("static JapaneseBeetle.x3 initialized"); 
} 
 
public class E24_JapaneseBeetle {   
    public static void main(String args[]) {     
        new JapaneseBeetle();   
    } 
}
// 运行结果
static Insect.x1 initialized 
static Beetle.x2 initialized 
static JapaneseBeetle.x3 initialized 
i = 9, 
j = 0 
Beetle.k initialized 
k = 47 j = 39 
JapaneseBeetle.m initialized 
m = 47 
j = 39
 
// 会先加载静态域,按照基类到子类的顺序
// 然后加载变量和构造器,按照基类到子类的顺序

7.10 总结

在开始一个设计时,一般应优先选择组合,必要的时候才用继承

 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值