读书笔记 --《Effective-Java》

 

 

Effective Java

代码编写原则

避免创建不必要的对象(通过重用同一对象,来避免创建多个对象)

一些对象的创建比其他对象的创建要昂贵得多。 如果要重复使用这样一个「昂贵的对象」,建议将其缓存起来以便重复使用。 不幸的是,当创建这样一个对象时并不总是很直观明显的。 假设你想写一个方法来确定一个字符串是否是一个有效的罗马数字。 以下是使用正则表达式完成此操作时最简单方法:static boolean isRomanNumeral(String s) {  return s.matches("^(?=.)M*(C[MD]|D?C{0,3})"    + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$"); } 这个实现的问题在于它依赖于 String.matches 方法。 虽然 String.matches 是检查字符串是否与正则表达式匹配的最简单方法,但它不适合在性能临界的情况下重复使用。 问题是它在内部为正则表达式创建一个Pattern 实例,并且只使用它一次,之后它就有资格进行垃圾收集。 创建 Pattern 实例是昂贵的,因为它需要将正则表达式编译成有限状态机(finite state machine)。为了提高性能,作为类初始化的一部分,将正则表达式显式编译为一个 Pattern 实例(不可变),缓存它,并在 isRomanNumeral 方法的每个调用中重复使用相同的实例:public class RomanNumerals {  private static final Pattern ROMAN = Pattern.compile(    "^(?=.)M*(C[MD]|D?C{0,3})"    + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");  static boolean isRomanNumeral(String s) {   return ROMAN.matcher(s).matches();  } }

注意自动装箱拆箱

优先使用基本类型而不是装箱的基本类型,也要注意无意识的自动装箱。多次的装修拆箱会造成性能上的巨大损耗,因为他们在使用时多次创建了一个又一个的对象。

消除过期的对象引用

清空对象引用应该是例外而不是规范。既只有在该对象确定已过期,而又不会被垃圾回收时,我们才需要去手动将其引用置空,使其能被回收。若能被GC自动回收,则我们不需要,也不应该去手动将其置空,这会让代码发生没必要的膨胀,同时如果对对象的过期与否判断错误还容易引发BUG。当一个类自己管理内存时,程序员应该警惕内存泄漏问题。

避免使用 Finalizer 和 Cleaner 机制

因为这两个机制并不稳定,无法保证程序运行的结果。

使用 try-with-resources 语句替代 try-finally 语句

使代码更加简洁,可读性更好。同时完善的内部机制能让资源在使用完毕后,更稳定的被关闭。

在公共类中使用访问方法而不是公共属性

即不要把属性设置为public,而是设置为private,通过一个public方法对其进行访问。这样我们可以随时通过修改该方法而对访问或者赋值进行限制,并对外部隐藏实现,这样一来,哪怕public方法被修改,也不影响原本代码的运行。

接口优于抽象类

接口可以多实现,而类只能单继承

将源文件限制为单个顶级类(一个.java文件只包含一个顶级类)

消除非检查警告(黄块)

优先考虑类型安全的异构容器

作为这种方法的一个简单示例,请考虑一个 Favorites 类,它允许其客户端保存和检索任意多种类型的favorite 实例。 该类型的 Class 对象将扮演参数化键的一部分。其原因是这 Class 类是泛型的。 类的类型从字面上来说不是简单的 Class ,而是 Class<T> 。 例如, String.class 的类型为 Class<String> ,Integer.class 的类型为 Class<Integer> 。 当在方法中传递字面类传递编译时和运行时类型信息时,它被称为类型令牌(type token)// Typesafe heterogeneous container pattern - API public class Favorites {  public <T> void putFavorite(Class<T> type, T instance);  public <T> T getFavorite(Class<T> type); } // Typesafe heterogeneous container pattern - client public static void main(String[] args) {  Favorites f = new Favorites();  f.putFavorite(String.class, "Java");  f.putFavorite(Integer.class, 0xcafebabe);  f.putFavorite(Class.class, Favorites.class);    String favoriteString = f.getFavorite(String.class);  int favoriteInteger = f.getFavorite(Integer.class);  Class<?> favoriteClass = f.getFavorite(Class.class);  System.out.printf("%s %x %s%n", favoriteString,   favoriteInteger, favoriteClass.getName()); } 总之,泛型 API 的通常用法(以集合 API 为例)限制了每个容器的固定数量的类型参数。 你可以通过将类型参数放在键上而不是容器上来解决此限制。 可以使用 Class 对象作为此类型安全异构容器的键。 以这种方式使用的Class 对象称为类型令牌。 也可以使用自定义键类型。 例如,可以有一个表示数据库行(容器)的DatabaseRow 类型和一个泛型类型 Column<T> 作为其键。

注解优于命名模式

常用注解:@Override 覆写@FunctionalInterface 函数时接口

使用标记接口定义类型

lambda 表达式优于匿名类,方法引用优于 lambda 表达式

并不绝对,要按实际情况取舍。他们的区别更多的是在代码的简洁和可读性上,性能上并没有太大的差距。如果方法引用看起来更简短更清晰,请使用它们;否则,还是坚持 lambda。

检查参数有效性

必要时进行防御性拷贝

返回空的数组或集合,不要返回 null

为所有已公开的 API 元素编写文档注释

如果循环终止后不需要循环变量的内容,那么优先选择 for 循环而不是 while 循环

for 更加简洁,代码可读性更好

for-each 循环优于传统 for 循环

生成随机数(不要再使用Random,而应该使用ThreadLocalRandom)

从 Java 7 开始,就不应该再使用 Random。在大多数情况下,选择的随机数生成器现在是ThreadLocalRandom。 它能产生更高质量的随机数,而且速度非常快。

通过接口引用对象

即:Set<Son> sonSet = new LinkedHashSet<>();  通过多态,让代码更加健壮,更加灵活。但是这其实是牺牲了一定的性能为条件的,若果追求的是性能而不是代码的健壮,应该确定其引用类型:LinkedHashSet<Son> sonSet = new LinkedHashSet<>(); 

接口优于反射

明智审慎地本地方法

为了提高性能,很少建议使用本地方法。 总之,在使用本地方法之前要三思。一般很少需要使用它们来提高性能。如果必须使用本地方法来访问底层资源 或本地库,请尽可能少地使用本地代码,并对其进行彻底的测试。本地代码中的一个错误就可以破坏整个应用程序。

明智审慎地进行优化(应该优先编写正确的程序,而不是快速的程序)

有三条关于优化的格言是每个人都应该知道的:比起其他任何单一的原因(包括盲目的愚蠢),很多计算上的过失都被归昝于效率(不一定能实现)。 —William A. Wulf 不要去计较效率上的一些小小的得失,在 97% 的情况下,不成熟的优化才是一切问题的根源。 —Donald E. Knuth 在优化方面,我们应该遵守两条规则: 规则 1:不要进行优化。 规则 2 (仅针对专家):还是不要进行优化,也就是说,在你还没有绝对清晰的未优化方案之前,请不要进行优化。 —M. A. Jackson

遵守被广泛认可的命名约定

数据类型相关

若需要精确答案就应避免使用 float 和 double 类型

float 和 double 类型特别不适合进行货币计算,因为不可能将 0.1(或 10 的任意负次幂)精确地表示为 float 或 double。使用 BigDecimal、int 或 long 进行货币计算。

不要使用原始类型

如果你使用原始类型,则会丧失泛型的所有安全性和表达上的优势。既类似于List这种,应当指定其泛型类型,而不应该不指定泛型类型而使用原始类型://这样做是错误的 List list = new Array();

当使用其他类型更合适时应避免使用字符串

当心字符串连接引起的性能问题(使用StringBuilder)

列表优于数组(列表的泛型限制比数组更加可靠)

异常相关

只针对异常的情况下才使用异常

异常应该只用于异常的情况下;他们永远不应该用于正常的程序控制流程。设计良好的 API 不应该强迫它的客户端为了正常的控制流程而使用异常。

对可恢复的情况使用受检异常,对编程错误使用 运行时异常

Java 程序设计语言提供了三种 throwable:受检异常(checked exceptions)、运行时异常(runtime exceptions) 和错误(errors)。如果期望调用者能够合理的恢复程序运行,对于这种情况就应该使用受检异常。用运行时异常来表明编程错误。你实现的所有非受检的 throwable 都应该是 RuntimeExceptiond 子类。总而言之,对于可恢复的情况,要抛出受检异常;对于程序错误,就要抛出运行时异常。不确定是否可恢复,就跑出为受检异常。不要定义任何既不是受检异常也不是运行异常的抛出类型。要在受检异常上提供方法,以便协助程序恢复。

避免不必要的使用受检异常

优先使用标准的异常

最常被重用的异常,IllegalArgumentException:  非 null 的参数值不正确IllegalStateException: 不适合方法调用的对象状态 NullPointerException: 在禁止使用 null 的情况下参数值为 nullIndexOutOfBoundsExecption:下标参数值越界 ConcurrentModificationException:在禁止并发修改的情况下,检测到对象的并发修改UnsupportedOperationException:对象不支持用户请求的方法 

抛出与抽象对应的异常

更高层的实现应该捕获低层的异常,同时抛出可以按照高层抽象进行解释的异常。尽管异常转译与不加选择地从低层传递异常的做法相比有所改进,但是也不能滥用它。 

每个方法抛出的异常都需要创建文档

在细节消息中包含失败一捕获信息

为了捕获失败,异常的细节信息应该包含“对该异常有贡献”的所有参数和字段的值。 例如, IndexOutOfBoundsException 异常的细节消息应该包含下界、上界以及没有落在界内的下标值。该细节消息提供了许多关于失败的信息。

不要忽略异常(即不要实现一个空的try catch块)

空的 catch 块会使异常达不到应有的目的。如果选择忽略异常, catch 块中应该包含一条注释,说明为什么可以这么做,并且变量应该命名为 ignored: 

线程相关

executor 、task 和 stream 优先于线程

ExecutorService ExecutorService exec = Executors.newSingleThreadExecutor();  exec.execute(runnable); exec.shutdown(); 

不要使用stop停止一个线程

千万不要使用 Thread.stop 方法。 要阻止一个线程妨碍另一个线程,建议的做法是让第一个线程轮询( poll ) 一个 boolean 字段,这个字段一开始为 false ,但是可以通过第二个线程设置为 true ,以表示第一个线程将终止自己。由于 boolean 字段的读和写操作都是原子的,程序员在访问这个字段的时候不再需要使用同步。

避免过度同步

为了避免活性失败和安全性失败,在一个被同步的方法或者代码块中,永远不要放弃对客户端的控制。 换句话 说,在一个被同步的区域内部,不要调用设计成要被覆盖的方法,或者是由客户端以函数对象的形式提供的方法 。从包含该同步区域的类的角度来看,这样的方法是外来的( alien ) 。这个类不知道该方法会做什么 事情,也无法控制它。根据外来方法的作用,从同步区域中调用它会导致异常、死锁或者数据损坏。通常来说,应该在同步区域内做尽可能少的工作。

不要依赖线程调度器

并发工具优于 wait 和 notify

ConcurrentHashMap 除了提供卓越的并发性之外,速度也非常快。在我的机器上,上面这个优化过的 intern 方法比 String.intern 快了不止 6 倍(但是记住, String.intern 必须使用某种弱引用,避免随着 时间的推移而发生内存泄漏)。并发集合导致同步的集合大多被废弃了。比如, 应该优先使用 ConcurrentHashMap ,而不是使用 Collections.synchronizedMap 。 只要用并发 Map 替换同步 Map ,就可以极大地提升并发应用程序的性能。 

并发集合(与同步集合的区别在于其使用分段锁提高了性能)

java.util.concurrent包中包含的并发集合类如下:  ConcurrentHashMap  CopyOnWriteArrayList  CopyOnWriteArraySet

同步访问共享的可变数据

除非读和写操作都被同步,否则无法保证同步能起作用。

保持失败原子性

一般而言,失败的方法调用应该使对象保持在被调用之前的状态。

明智审慎的使用延迟初始化(在大多数情况下,常规初始化优于延迟初始化)

与大多数优化一样,延迟初始化的最佳建议是「除非需要,否则不要这样做」。延迟初始化是一把双刃剑。它降低了初始化类或创建实例的成本,代价是增加了访问延迟初始化字段的成本。根据这些字段中最终需要初始化的部分、初始化它们的开销以及初始化后访问每个字段的频率,延迟初始化实际上会损害性能(就像许多「优化」一样)。在大多数情况下,常规初始化优于延迟初始化。如果您使用延迟初始化来取代初始化的循环(circularity),请使用同步访问器,// Lazy initialization of instance field - synchronized accessor private FieldType field; private synchronized FieldType getField() { if (field == null) field = computeFieldValue(); return field; } 如果需要在静态字段上使用延迟初始化来提高性能,使用 lazy initialization holder class 模式。 // Lazy initialization holder class idiom for static fields private static class FieldHolder { static final FieldType field = computeFieldValue(); } private static FieldType getField() { return FieldHolder.field; } 第一次调用 getField 时,它执行 FieldHolder.field,导致初始化 FieldHolder 类。这个习惯用法的优点是 getField 方法不是同步的,只执行字段访问,所以延迟初始化实际上不会增加访问成本。典型的 VM 只会同步字段访问来初始 化类。初始化类之后,VM 会对代码进行修补,这样对字段的后续访问就不会涉及任何测试或同步。

双重锁模式范例

如果需要使用延迟初始化来提高实例字段的性能,请使用双重检查模式。这个模式避免了初始化后访问字段时的锁定成本。这个模式背后的思想是两次检查字段的值(因此得名 double check):一次没有锁定, 然后,如果字段没有初始化,第二次使用锁定。只有当第二次检查指示字段未初始化时,调用才初始化字段。由于初 始化字段后没有锁定,因此将字段声明为 volatile 非常重要。下面是这个模式的示例:// Double-check idiom for lazy initialization of instance fields private volatile FieldType field; private FieldType getField() { FieldType result = field; if (result == null) { // First check (no locking) synchronized(this) { if (field == null) // Second check (with locking) field = result = computeFieldValue(); } } return result; }  关于变量result存在的必要解释:该变量的作用是确保 field 在已经初始化的情况下只读取一次。 虽然不是严格必需的,但这可能会提高性能,而且与低级并发编程相比,这更优雅。虽然您也可以将双重检查模式应用于静态字段,但是没有理由这样做:the lazy initialization holder class idiom 是更好的选择。

静态字段上使用延迟初始化的推荐做法

如果需要在静态字段上使用延迟初始化来提高性能,使用 lazy initialization holder class 模式。 // Lazy initialization holder class idiom for static fields private static class FieldHolder { static final FieldType field = computeFieldValue(); } private static FieldType getField() { return FieldHolder.field; } 第一次调用 getField 时,它执行 FieldHolder.field,导致初始化 FieldHolder 类。这个习惯用法的优点是 getField 方法不是同步的,只执行字段访问,所以延迟初始化实际上不会增加访问成本。典型的 VM 只会同步字段访问来初始 化类。初始化类之后,VM 会对代码进行修补,这样对字段的后续访问就不会涉及任何测试或同步。

尽可能不要实现序列化接口(因为这会带来更多的安全问题以及性能问题)

对于实例控制,枚举类型优于 readResolve(因为内存原因,不适用于Android)

类设计原则

用静态工厂构造方法待替构造方法

优点:静态工厂方法的一个优点是,不像构造方法,它们是有名的。静态工厂方法的第二个优点是,与构造方法不同,它们不需要每次调用时都创建一个新对象。(可以根据实际情况选择是否使用单例,或者控制实例数量)静态工厂方法的第三个优点是,与构造方法不同,它们可以返回其返回类型的任何子类型的对象。静态工厂的第四个优点是返回对象的类可以根据输入参数的不同而不同。静态工厂的第 5 个优点是,在编写包含该方法的类时,返回的对象的类不需要存在。(既后续实现了新的子类,也可以在该方法中返回而不用修改其他代码)缺点:没有公共或受保护构造方法的类不能被子类化。因为子类被构造时必须先调用父类的构造方法,如果类的实现是静态工厂,而且将构造方法标志为private,呢么就会出错,所以构造方法被私有化的类是不能被继承的。若设计该类时,并不打算让该类被继承,呢么可以使用这种方法,并将类声明为final,在确时需要对类进行扩展或修改时,可以考虑使用组合,而不是继承。

构造参数过多时使用Builder

优点:Builder 模式非常灵活。 单个 builder 可以重复使用来构建多个对象。 builder 的参数可以在构建方法的调用之间进行调整,以改变创建的对象。 Builder 可以在创建对象时自动填充一些属性,例如每次创建对象时增加的序列号。防止类构造参数列表过长,使得程序可读性差。缺点:为了创建对象,首先必须创建它的 builder。虽然创建这个 builder 的成本在实践中不太可能被注意到,但在性能关键的情况下可能会出现问题。而且,builder 模式比伸缩构造方法模式更冗长,因此只有在有足够的参数时才值得使用它,比如四个或更多。但是请记住,如果希望在将来添加更多的参数。但是,如果从构造方法或静态工厂开始,并切换到 builder,当类演化到参数数量失控的时候,过时的构造方法或静态工厂就会面临尴尬的处境。因此,所以,最好从一开始就创建一个 builder。

范例代码

public class NutritionFacts {  private final int servingSize;  private final int servings;  private final int calories;  private final int fat;  private final int sodium;  private final int carbohydrate;  private NutritionFacts(Builder builder) {   servingSize = builder.servingSize;   servings = builder.servings;   calories = builder.calories;   fat   = builder.fat;   sodium  = builder.sodium;   carbohydrate = builder.carbohydrate;  }  public static class Builder {   // Required parameters   private final int servingSize;   private final int servings;   // Optional parameters - initialized to default values   private int calories  = 0;   private int fat   = 0;   private int sodium  = 0;   private int carbohydrate = 0;   public Builder(int servingSize, int servings) {    this.servingSize = servingSize;    this.servings = servings;   }   public Builder calories(int val) {    calories = val;      return this;   }   public Builder fat(int val) {    fat = val;       return this;   }   public Builder sodium(int val) {    sodium = val;      return this;   }   public Builder carbohydrate(int val) {    carbohydrate = val; return this;   }   public NutritionFacts build() {    return new NutritionFacts(this);   }  } }

对于那些只有静态方法或静态属性的类,私有化构造方法,防止被实例化

依赖注入优于硬连接资源

有些时候,我们的类需要使用其他的类的功能来实现我们的功能,最典型的例如代理类,这时候如果我们写死了内部使用的类,呢么在我们需要其功能实现发生一些改变的时候,我们就必须修改源码,而如果我们通过依赖注入实现,例如通过set方法或者构造方法等,从外部传入其功能类,这样一来,如果我们在其他地方需要不同的方法实现,只需要传入一个新的类便可以做到,而不需要去动源码。这样的代码更加健壮,也更加易于扩展。

重写 equals 方法时遵守通用约定

自反性: 对于任何非空引用 x, x.equals(x) 必须返回 true。对称性: 对于任何非空引用 x 和 y,如果且仅当 y.equals(x) 返回 true 时 x.equals(y) 必须返回 true。传递性: 对于任何非空引用 x、y、z,如果 x.equals(y) 返回 true, y.equals(z) 返回 true,则x.equals(z) 必须返回 true。一致性: 对于任何非空引用 x 和 y,如果在 equals 比较中使用的信息没有修改,则 x.equals(y) 的多次调用必须始终返回 true 或始终返回 false。对于任何非空引用 x, x.equals(null) 必须返回 false。重写equals,则必须重写hashCode方法

范例

始终重写 toString 方法

谨慎地重写 clone 方法

使类和成员的可访问性最小化

最小化可变性

即对于每个方法,每个属性,都考虑是否需要变化,若不需要,则声明为final。不可变的类不存在竞争态,也就不存在线程安全问题。要使一个类不可变,请遵循以下五条规则: 1. 不要提供修改对象状态的方法(也称为 mutators)。 2. 确保这个类不能被继承。3. 把所有属性设置为 final。 4. 把所有的属性设置为 private。 5. 确保对任何可变组件的互斥访问。 如果你的类有任何引用可变对象的属性,请确保该类的客户端无法获得对这些对象的引用。 切勿将这样的属性初始化为客户端提供的对象引用,或从访问方法返回属性。 在构造方法,访问方法和 readObject 方法中进行防御性拷贝,即对外接口给出的是原始对象的拷贝,这样一来就不会被从外部修改。

组合优于继承

与方法调用不同,继承打破了封装。 换句话说,一个子类依赖于其父类的实现细节来保证其正确的功能。 父类的实现可能会从发布版本不断变化,如果是这样,子类可能会被破坏,即使它的代码没有任何改变。 因此,一个子类必须与其超类一起更新而变化,除非父类的作者为了继承的目的而专门设计它,并对应有文档的说明。在决定使用继承来代替组合之前,你应该问自己最后一组问题。对于试图继承的类,它的 API 有没有缺陷呢? 如果有,你是否愿意将这些缺陷传播到你的类的 API 中?继承传播父类的 API 中的任何缺陷,而组合可以让你设计一个 隐藏这些缺陷的新 API。 总之,继承是强大的,但它是有问题的,因为它违反封装。 只有在子类和父类之间存在真正的子类型关系时才适用。 即使如此,如果子类与父类不在同一个包中,并且父类不是为继承而设计的,继承可能会导致脆弱性。 为了避免这种脆弱性,使用合成和转发代替继承,特别是如果存在一个合适的接口来实现包装类。 包装类不仅比子类更健壮,而且更强大。

要么设计继承并提供文档说明,要么禁用继承

类层次结构优于标签类

有时你可能会碰到一个类,它的实例有两个或更多的风格,并且包含一个标签属性,表示实例的风格,根据标签的不同类的方法有着不同的运行效果。这样的类是很常见的,可这样的类的扩展性也是很糟糕的,因为你每添加一个新的风格,就得加一个新的标签属性值,并且在所有方法中使用if或者switch添加条件,这会让你的代码变得原来原臃肿。考虑使用以下方法代替实现:通过依赖注入,让类的实现基于依赖注入的属性类,这样一来,如果增加了新的风格,你也只需要实现一个新的风格类,并利用其实例化出对应的类即可。

使用静态成员类而不是非静态类

如果你声明了一个不需要访问宿主实例的成员类,总是把 static 修饰符放在它的声明中,使它成为一个静态成员类,而不是非静态的成员类。因为非静态成员类会隐式持有一个对外部类的引用,这容易造成内存泄漏问题。

优先考虑泛型

使用泛型来构造你的类,往往能让你的代码更加健壮。

使用枚举类型替代整型常量(Android中不推荐,因为会多消耗很多内存资源)

使用实例属性替代序数

永远不要从枚举的序号中得出与它相关的值; 请将其保存在实例属性中:public enum Ensemble {  SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5),  SEXTET(6), SEPTET(7), OCTET(8), DOUBLE_QUARTET(8),  NONET(9), DECTET(10), TRIPLE_QUARTET(12);  private final int numberOfMusicians;    Ensemble(int size) { this.numberOfMusicians = size; }  public int numberOfMusicians() { return numberOfMusicians; } }

使用 EnumSet 替代位属性

使用 EnumMap 替代序数索引

使用接口模拟可扩展的枚举

虽然不能编写可扩展的枚举类型,但是你可以编写一个接口来配合实现接口的基本的枚举类型,来对它进行模拟。public interface Operation {  double apply(double x, double y); } public enum BasicOperation implements Operation {  PLUS("+") {   public double apply(double x, double y) { return x + y; }  },  MINUS("-") {   public double apply(double x, double y) { return x - y; }  },  TIMES("*") {   public double apply(double x, double y) { return x * y; }  },  DIVIDE("/") {   public double apply(double x, double y) { return x / y; }  };  private final String symbol;  BasicOperation(String symbol) {   this.symbol = symbol;  }  @Override public String toString() {   return symbol;  } }

最小化局部变量的作用域

在首次使用的地方声明它。

接口设计原则

接口的默认方法实现

public interface Comparable<T> { default int compareTo(T other) { return 0; } } 在java1.8后,接口可以拥有方法的默认实现,既接口不再只能有抽象方法,也能有具体方法,但是该特性应该用于在旧接口添加新方法的时候,因为这样一来,就不需要原本所有实现该接口的类都去重写一个新添加的方法。

接口仅用来定义类型

常量接口模式是对接口的糟糕使用。既接口只用来做常量的存放容器,这违反了接口的设计原则,容易造成误解与代码的混乱。


 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值