Effective Java 第二版读书笔记

【本文对许久前转载的部分过时博文进行了替换,所以发表时间可以不参考】

        本篇学习笔记为通读Effective Java 第二版后总结的主要篇幅,不涉及太多繁冗的描述性内容,在此补充一下。对规则的具体应用场景,可结合使用时的具体情况灵活而定,适合的才是最好的,以下为主要描述:
NO.1、考虑用静态工厂方法代替构造函数,即通过工厂方法获得实例而非 new ()
(1)好处
       有名字方便调用,不像诸多构造器,调用时不知道选哪一个
       BigDecimal.getInstanceFromString(...)
       可单例,不创建新实例
       可以返回任何子类型,方法中可做任何事情,返回任意类型。
       代码更简洁

Map<String,List<String>> m= new HashMap<String, List<String>>();
-->
public static <k,v> HashMap<k,v> newInstance(){
       return new HashMap<k,v>();
}
Map<String,List<String>> m= HashMap.newInstance()

(2)总结
       方法名尽量采用valueOf、of、getInstance、newInstance、getType、newType
       与构造方式各有长处,灵活使用
NO.2、遇到多个构造器参数时要考虑构造器
       此时采用Builder方式(尤其是当大多数参数时可选时)替代重叠构造器、JavaBean方式,既能保证重叠构造器那样的安全性,也能保证JavaBeans模式那么好的可读性:

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;

    public static class Builder {
        // 必输参数
        private final int servingSize;
        private final int servings;

        // 可选参数 - 初始化成默认值
        private int calories      = 0;
        private int fat           = 0;
        private int carbohydrate  = 0;
        private int sodium        = 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 carbohydrate(int val)
            { carbohydrate = val;  return this; }
        public Builder sodium(int val)
            { sodium = val;        return this; }
             
              // 构造产品
        public NutritionFacts build() {
            return new NutritionFacts(this);
        }
    }
      
    // 构造器需要一个builder对象
    private NutritionFacts(Builder builder) {
        servingSize  = builder.servingSize;
        servings     = builder.servings;
        calories     = builder.calories;
        fat          = builder.fat;
        sodium       = builder.sodium;
        carbohydrate = builder.carbohydrate;
    }

    public static void main(String[] args) {
        NutritionFacts cocaCola = new NutritionFacts.Builder(240,.
            calories(100).sodium(35).carbohydrate(27).build();
    }
}

NO.3、用私有的构造函数或枚举强化Singleton可靠性
示例:

 

/**
 * 普通的Singletone实现。
 */  
class TestSingleton1 {  
  private static final TestSingleton1 INSTANCE = new TestSingleton1();  
  public static TestSingleton1 getInstance() {  
    return INSTANCE;  
  }  
  private TestSingleton1() {  
  }  
}  
/**
 * 异常强化了的Singletone实现。
 */  
class TestSingleton2 {  
  private static final TestSingleton2 INSTANCE = new TestSingleton2();  
  public static TestSingleton2 getInstance() {  
    return INSTANCE;  
  }  
  private static boolean initSign;  
  private TestSingleton2() {  
    if (initSign) {  
      throw new RuntimeException("实例只能建造一次");  
    }  
    initSign = true;  
  }  
}  
/**
 * 枚举实现的Singleton,最安全
 */  
enum TestSingleton3 {  
  INSTANCE;  
  public static TestSingleton3 getInstance() {  
    return INSTANCE;  
  }
}

NO.4、通过私有构造器避免实例化
       私有构造器不能实例化,即便插入异常或反射也不会实例化,当然也不能被继承

 

public class TestPrivateConstruct {  
  public static void main(String[] args) {  
    new TestPrivateConstruct();  
  }  
  // 禁止任何类调用这个构造器  
  private TestPrivateConstruct() {  
    throw new AssertionError();  
  }  
}  

NO.5、避免创建不必要的、重复的对象
       尽可能地避免创建对象,而不是不创建对象(也不符合OO的思想)

 

×String s = new String("test");
√String s = "test";
×Boolean(String)
√Boolean.vauleOf(String)
public Set<K> keySet() {
√  Set<K> ks = keySet; //存储到keySet成员域
    return (ks != null ? ks : (keySet = new KeySet()));
}

       优先使用基本类型而非装操作:

 

public static void main(String[] args) {
×     Long sum = 0L;
√     long sum = 0L;
       for (long i = 0; i < Integer.MAX_VALUE; i++) {
              sum += i;
       }
       System.out.println(sum);
}

       对于确定了的对象,尽量只执行一次实例化,采用static、成员变量等等方式。

 

public boolean isBabyBoomer() {
×    //实例化对象
}
static {
√    //实例化对象
}
public boolean isBabyBoomer() {
      //
}

NO.6、消除过期的对象引用
       虽然JAVA提供了垃圾回收器对不可达对象的内存进行自动回收,但已分配且不再使用的对象却长期占用内存造成内存泄露,
       因为GC不会回收这些对象。主要关注:过期引用、缓存两种情况,其中缓存类可借助:WeakHashMap、WeakReference、软引用等方式进行,也可手动进行管理。

 

×return elements[--size]; //过期元素没有回收
  Object result =elements[--size];
√elements[size] = null;//只要外界不再引用即可回收
  return result; 

       消除过期引用最好的方法是让引用结束其任命周期,如在小的作用域内退出后自动结束就没有必要,可借助Heap Profiler进行内存泄露的分析。
NO.7、避免使用终结
       尽量不用类的finalizer(),除非忘了对象显示回收或用来回收不关键的本地资源时,因该方法线程的优先级很低,不能保证会被及时执行(执行时机不确定),JVM也总会延迟终结函数的执行。即便是System.gc和System.runFinalization,也不能保证终结立即执行,终结方法创建和销毁对象相对更慢。
       急需回收对象,可以使用tyr—finally。
NO.8、在改写equals方法时请遵守通用约定
       不需要override equals时就不要去麻烦,需要改写equals方法时,严格遵守约定,避免一些运行中未知的问题出现,规范如下:
       1、自反性,即x.equals(x)为true
       2、对称性,即当且仅当x.equals(y)为true时y.equals(x)也一定为true
       3、传递性,即对任意的x,y,z,如果x.equals(y)为true,并且y.equals(z)也为true,那么x.equals(z)也必须为true
       4、一致性,即对于任意的x,y,如果x,y都没有被修改的话,那么多次调用x.equals(y)要么一致地返回ture,要么一致地返回false
       5、对于非空的引用x,x.equals(null)一定要返回false

       其中:
       1、用==操作符检查实参是否为指向对象的同一个引用。
       2、使用instanceOf检查实参是否是正确的类型。
       3、instanceof之后,把实参转换成正确的类型。
       4、检查实参的域与当前对象的域值是否相等。
       5、编写完equals方法后,检查是否满足等价关系。

 

    public boolean equals(Object o)
    {
           if(o== this) return true;
           if(!(o instanceof xxxx) return false;
           xxx in = (xxx)o;
           return ……..
    }

       equals过程:
       避免事务处理过多
       避免不安全的依赖
       避免转换为其他类型
NO.9、覆盖equals时总是要覆盖hashCode
       否则违反Object.hashCode约定,导致该类无法结合所有基于散列的集合一起正常动作,这些集合包括HashMap、HashSet、Hashtable等,如没有覆盖hashCode方法而进行以下操作:
       Map m = new HashMap();
       m.put(new PhoneNumber(408,863,3334),"xujian")
       当调get(new PhoneNumber(408,863,3334))时得到null,因为有两个实例,一个是put一个是get,hashCode不同。
       有关hashCode的覆盖约定这里不再赘述。
NO.10、始终要覆盖toString
       默认的toString方法返回的信息无意义,可以返回有意义的格式化数据。
NO.11、谨慎改写clone方法
       对象要克隆,必须实现Cloneable的Clone()方法:

 

    public Object clone()  
    {  
    try  
    {  
    return super.clone();  
    }  
    catch(CloneNotSupportedException e)  {}  
    }

       当你要clone的类里面含有可修改的引用字段的时候(不保留原有引用关系)

 

    public Object clone() throws CloneNotSupportedException  
    {
           Stack Result  = (Stack)super.clone();  
           Result.elements = (Object[])elements.clone();//elements是stack类中可修改的引用字段
           Return result;  
    }

       注意:使用Object中的默认clone对某个类进行克隆时,任何类型的数组属性成员都只是浅复制,即克隆出来的数组与原来类中的数组指向同一存储空间,其他引用也是这样,只有基本类型才深复制。

       深拷贝:逐一复制并创建新实例

 

//递归地深层复制每个链表节点对象
  Entry deepCopy(){
       return new Entry(key, value, next == null ? null : next.deepCopy());
  }
//如链表较长,容易导致栈溢出,因此可以:
Entry deepCopy(){
       Entry result = new Entry(key, value, next);
       for(Entry p = result; p.next != null; p = p.next){
          p.next = new Entry(p.next.key, p.next.value, p.next.next);
       }
       return result;
}
@Override public HahsTable clone() {
       try{
              HashTable result = (HashTable)super.clone();
              result.buckets = new Entry[buckets.length];
              // 采用循环的方式对每个桶引用的单链表进行深层拷贝
              for(int i = 0; i < buckets.length; i++){
                  if(buckets[i] != null){
                    result.buckets[i] = buckets[i].deepCopy();
              }
           }
              return result;
       } catch (CloneNotSupportedException e) {
              throw new AssertionError();
             }
}

NO.12、考虑实现Comparable接口
       该方法是Comparable接口的唯一方法,允许进行简单的相等比较,也允许执行顺序比较,一个类实现comparable接口就表明其实例具有内置的排序关系。Java中所有的值类都实现了Comparable。将当前对象与指定对象进行顺序比较时,返回负整数、0或正整数(<、=、>),如果指定对象的类型无法比较,则抛出ClassCastException或者NullPointException异常,compareTo方法应遵守:自反性、对称性、传递性和非空性的限制条件。在实现数值比较的compareTo方法时还要防止值域溢出。
NO.13、使类和成员的可访问能力最小化
       通过访问修饰符,可把模块之间的耦合程度降到最低,控制安全性:
       public表示这个类在任何范围都可用。
       protected表示只有子类和包内的类可以使用
       private-package(default) 包级私有,表示在包内可用
       private表示只有类内才可以用
       尽可能使每个类或者成员不被外界访问,即使用尽可能最小的访问级别。除公有静态final域的情形外,公有类不应包含公有域,且确保有静态final域所引用的是不可变的。
NO.14、在公有类中使用访问方法而非公有域
       方法可以做很多事情,大家都懂的——
NO.15、使可变性最小化
       不可变的类不容易出错,更加安全,如String、基本类型的包装类、BigInteger和BigDecimal。这些类实例不能再修改,整个生命周期不变,当然加上final是另一回事。
       不可变类遵循下面五条规则:
       ①不要提供任何会修改对像的方法;
       ②保证没有可被子类改写的方法;
       ③使所有的域都是final的;
       ④使所有的域都成为私有的;
       ⑤保证对于任何可变组件的互斥访问。

       使一个类成为不可变类有三种方法:
       ①类声明为final;
       ②每一个方法都成为final的,好处在于其子类可以继续扩展新的方法;
       ③把类的构造函数声明为私有的或者包级私有的,增加静态工厂方法,来代替公有的构造函数
NO.16、组合优先于继承
       意思就是用组合更灵活,如果有条件用组合尽量不要用继承,因为继承破坏了封装,导致键壮性不足,不变扩展。
NO.17、要么为继承而设计并提供文档说明,要么就禁止继承(final类、所有的构造函数变为私有)
       这个因人而异,结合实际情况为准,设计继承类时适当考虑:
       1、描述改写每个方法带来的影响
       2、被继承的类的构造函数一定不能调用可被改写或覆盖的方法,因为超类的构造函数会在子类的构造函数之前运行,所以子类中改下版本的方法将会在子类的构造函数运行之前就被调用
NO.18、接口优于抽象类
       这个是OO设计的一个原则,了解抽象类和接口的主要区别以及什么时候需要用抽象类和接口即可,大家都懂得,不做赘述。
       抽象类:可包含具体实现和成员声明,作公共抽象时使用,可继承一些特性
       接口:行为约束,方法PUBLIC
NO.19、接口只用于定义类型
       类实现接口时,接口就充当可以引用这个类的实例的类型。为其他目的而定义接口是不恰当的(常量接口违反了上面的条件,不值得效仿):
       1、导致把实现细节泄露到该类的导出API中。
       2、如果类被修改,不需要使用这些常量,还须实现这个接口,以确保二制兼容性。
       3、如果非final类实现常量接口,所有子类的命名空间也会被接口中的常量污染。
       OK,既然是仅定义类型,那就不可能提供再进行类型变量声明,如定义一个常量是错误的。但可通过类、接口、枚举等作为参数进行导出:

 

public class PhysicalConstants {
  private PhysicalConstants() { }  // 私有构造器
  public static final double AVOGADROS_NUMBER   = 6.02214199e23;
}

NO.20、类层次优于标签类
       意思就是利用继承保持层次关系,多态与派生。
NO.21、用类代替结构
       意思是不要把类单纯的用于结构类型定义,要包含OO的思想。

 

    class Point  
    {  
     private float x;  
     private float y;  
    } 
    class Point  
     {  
      private float x;  
      private float y;  
      public Point(float x,float y)  
      {  
            this.x=x;  
            this.y=y;  
      }  
       public float getX(){retrun x;}  
       public float getY(){return y;}  
       public void setX(float x){this.x=x;}  
       public void setY(float y){this.y=y;}  
     }  

       当然,如果类是包级私有的,或是一个私有的嵌套类,则直接暴露值域也可以。
NO.22、用类来代替enum结构
       定义类来代表枚举类型的单个元素,并且不提供任何公有的构造函数,好处多多。
NO.23、用类和接口来代替函数指针
       用类和接口的应用可以实现C中函数指针一样的功能,下面的这个例子已经用烂了。

 

    public interface Comparator{  
      public int compare(Object o1,Object o2);  
    }

       为了实现指针的模式,声明一个接口来表示该策略,并且为每个具体策略声明一个实现了该接口的类,如果策略只用一次,那么定义匿名类;如果策略类反复使用,那么定义为私有的的静态成员类
NO.24、用函数对象表示策略
       例如一个类仅作比较使用,然后通过接口定义比较策略。

 

public class StringLengthComparator implements Comparator<String> {
       private StringLengthComparator() {}
       public int compare(String s1, String s2) {
              return s1.length() - s2.length();
       }
}
Arrays.sort(strArr,new Comparator<String>(){
       public int compare(String s1, String s2) {
              return s1.length() - s2.length();
}});

NO.25、优先考虑静态成员类
    部分描述不一定正确,大家自行鉴别:
        嵌套类只为它的外围类提供服务。
        嵌套类分为四种:静态成员类、非静态成员类、匿名类和局部类(后面三种称为内部类)
       补充一点:静态类(static class)即定义了静态方法,静态变量,静态代码块或者内部静态类的类。这些静态成员不需要实例化即可直接引用,类的内部也不能使用this。
       静态成员类的一种常见用法是作为公有的辅助类,仅当与它的外部类一起使用时才有意义,如:Calculator.Operation.PLUS。
       非静态成员类一种常见用法是定义一个Adapter,允许外部类的实例被看作是另一个不相关的类的实例。
       如果成员类的每个实例都需要一个指向其外围实例的引用,则把成员类做成非静态的;否则就做成静态的。
       如果嵌套类的实例可以在它外围类的实例之外独立存在,这个嵌套类就必须是静态成员类,意思是A中定义了A1,B也想用A1,则A1定义为静态成员类。
NO.26、不使用原生态类型
       不要在新代码中使用。原生态类型只为了与引入泛型之前的遗留代码进行兼容和互用而提供的。另外Set<Object>是个参数化类型,表示可以包括任何对象类型的一个集合;Set<?>则是一个通配符类型,表示只能包含某种未知对象类型的一个集合;Set则是个原生态类型,它脱离了泛型系统。前两者是安全的,最后一种不安全。
       原生态类型:List
       参数化的类型:List<String>
       泛型:List<E>
       有限制类型参数:List<E extends Number>
       形式类型参数:E
       无限制通配符类型:List<?>
       有限制通配符类型:List<? extends Number>
       递归类型限制:List <T extends Comparable<T>>
       泛型方法: static<E> List<E> asList(E[] a)
NO.27、消除非受检警告
       泛型编程时,会遇到许多编译器警告,不要忽略它们。
       尽可能地消除每一个非受检警告。如果消除了所有警告,就可以确保代码是类型安全的。如果无法消除警告,同时可以证明引起警告的代码是类型安全的,只有在这种情况下才可以用一个@SuppressWarnings("unchecked")注解来禁止这条警告
NO.28、列表优先于数组
       数组提供了运行时的类型安全,但是没有编译时的类型安全,反之,对于泛型也一样。一般来说,数组和泛型不能很好地混合使用。如果你发现自己将它们混合起来使用,并且得到了编译时错误或者警告,你的第一反应就应该是用列表来代替数组。
       List<Object> ol = new ArrayList<Long>(); //列表在编译时就不能通过
NO.29、优先考虑泛型
       只要时间允许,就把现有的类型都泛型化
NO.30、优先考虑泛型方法
       好处如泛型一样,静态工厂方法尤其适合泛型方法:

 

public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
              Set<E> result = new HashSet<E>(s1);
              result.addAll(s2);
              return result;
}

NO.31、利用有限制通配符?来提升API灵活性
       通配符类型使应用更灵活,尤其是公共的类库。
       XXX<? extends T> x:使用<? extends T>定义的引用x,x可以用来接受类型参数为T及T的所有子类类型的实例
       XXX<? super T> x:使用<? super T>定义的引用x,x可以用来接受类型参数为T及T的所有父类类型的实例。

       如果参数化类型表示一个T生产者就使用<? extends T>;
       如果它表示一个T是消费者,就使用<? super T>,Comparable和Comparator都是消费者,适合于<? super XXX>。

 

static <T extends Comparable<? super T>> T max(List<? extends T> list)
static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2) ;
//错误
Set<Number> nubers =Union.union(integers, doubles);
//正确,显示的指定返回类型,而不使用类型推导:
//必须是具体类型E或extend Object,不可由?来自动推导
Set<? extends Number> nubers = union(integers, doubles); 

       类型参数和通配符之间具有双重性,尽量通配符。
       public static <E> void swap(List<E> list, int i, int j);
       public static void swap(List<?> list, int i, int j);
       如类型参数只在方法参数声明列表中出现,方法体没出现,可用通配符取代它。
       如是无限制的类型参数<E>,就用无限制的通配符取代它<?>。
       如是有限制的类型参数<E extends Number>,就用有限制的通配符取代它<? extends Number>。
       Comparable<? super T>优先于Comparable<T>
       Comparator<? super T>优先于Comparator<T>

示例:

 

public void pushAll(Iterable<E> src) {
       for (E e : src)
     push(e);
}
Stack<Number> numberStack = new Stack<Number>();//1
Iterable<Integer> integers = Arrays.asList(3, 1, 4, 1, 5, 9); //2
numberStack.pushAll(integers); //出错,Iterable<Integer>不是Iterable<Number>子类型
修改为:
public void pushAll(Iterable<? extends E> src) {
       for (E e : src)
              push(e);
}

List<? extends E> src这种集合只能读不能写,即只能传进参数(如add方法),而不能返回E类型元素

出栈:

 

public void popAll(Collection<E> dst) {
       while (!isEmpty())
              dst.add(pop());
}
Collection<Object> objects = new ArrayList<Object>();//1
numberStack.popAll(objects);//2
System.out.println(objects);
public void popAll(Collection<? super E> dst) {
       while (!isEmpty())
              dst.add(pop());
} 

出错:因为Collection<Object>不是Collection<Number>的子类型
NO.32、优先考虑类型安全的异构容器
        有没有一种这样的Map,即可以存放各种类型的对象,但取出时还是可以知道其确切类型,可以用Class对象作为键。

 

private Map<Class<?>, Object> favorites = new HashMap<Class<?>, Object>();
public <T> void putFavorite(Class<T> type, T instance) {
    if (type == null)
        throw new NullPointerException("Type is null");
    //防止客户端传进原生的Class对象,虽然这会警告,但这就不能确保后面instance实例为type类型了,所以在这种情况下强制检查
    favorites.put(type, type.cast(instance));
}
public <T> T getFavorite(Class<T> type) {
    //返回的还是存入时真真类型
    return type.cast(favorites.get(type));
}

NO.33、用enum代替int常量、String常量
       大家都懂得……
       没有可以访问的构造器,枚举类型是真正的final,客户端即不能创建枚举类型的实例,也不能对它进行扩展。
       诸如static final int APPLE_FUJI = 0这样的int常量枚举缺点太多:
       1、类型不安全容易错乱
       2、属于编译时常量INT变化需重新编译
       3、toString打印的数字没实际意义
       4、遍历一组中所有的int枚举常量,没有可靠的方法

       String常量:
       1、编译时常量
       2、性能问题

 

 

 

// 内部可作遍历和其他操作
public enum Operation {
       PLUS("+") {
              double apply(double x, double y) {
                     return x + y;
              }
       },
       MINUS("-") {
              double apply(double x, double y) {
                     return x - y;
              }
       };
       private final String symbol;//操作符:+ - * /

       Operation(String symbol) {//构造函数,存储操作符供toString打印使用
              this.symbol = symbol;
       }

       @Override
       //重写Enum中的打印name的性为
       public String toString() {
              return symbol;
       }

       //抽像方法,不同的常量具有不同的功能,需在每个常量类的主体里重写它
       abstract double apply(double x, double y);
 private static final Map<String, Operation> stringToEnum = new HashMap<String, Operation>();
       static { // 从name到枚举常量转换到从某个域到枚举常量的转换
              for (Operation op : values())
                     stringToEnum.put(op.toString(), op);
       }

       // 根据操作符来获取对应的枚举常量,如果没有返回null,模拟valueOf方法
       public static Operation fromString(String symbol) {
              return stringToEnum.get(symbol);
       }
}

 

NO.34、不要使用ordinal,用实例域代替序数
        意思是不要用ordinal()方法来获取枚举中的顺序,变更顺序不方便,可自定义枚举值(int)来排序。
       SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5), SEXTET(6), SEPTET(7);
NO.35、用EnumSet代替位域
       public static final int STYLE_BOLD = 1 << 0;//1 字体加粗
       public static final int STYLE_ITALTC = 1 << 1;// 2 斜体
       EnumSet.of(Style.BOLD, Style.ITALIC)
NO.36、用EnumMap代替序数索引
与枚举类型键一起使用的专用 Map 实现。

 

 

for (Herb h : garden) {
    //根据序数取出对应的容器再放入
    herbsByType[h.type.ordinal()].add(h);
}
// 使用EnumMap并按照植物种类(枚举类型)来分类
Map<Herb.Type, Set<Herb>> herbsByType = new EnumMap<Herb.Type, Set<Herb>>(
              Herb.Type.class);
for (Herb.Type t : Herb.Type.values())//初始化
       herbsByType.put(t, new HashSet<Herb>());
for (Herb h : garden)//进行分类
       herbsByType.get(h.type).add(h);

NO.37、用接口模拟可伸缩的枚举
       枚举类型是不能被扩展的(继承),但使用接口可以解决这一问题,解决办法是让枚举类实现同一接口

 

// 枚举接口
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;
              }
       };
       private final String symbol;

       BasicOperation(String symbol) {
              this.symbol = symbol;
       }
       @Override
       public String toString() {
              return symbol;
       }
}

//扩展运算
public enum ExtendedOperation implements Operation {
       EXP("^") {
              public double apply(double x, double y) {
                     return Math.pow(x, y);
              }
       };
       private final String symbol;

       ExtendedOperation(String symbol) {
              this.symbol = symbol;
       }

       @Override
       public String toString() {
              return symbol;
       }
}

//测试
private static <T extends Enum<T> & Operation> void test(Class<T> opSet,
              double x, double y) {
       for (Operation op : opSet.getEnumConstants())//失去Enum特性,使用反射
              System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));
}
public static void main(String[] args) {
       double x = Double.parseDouble(args[0]);
       double y = Double.parseDouble(args[1]);
       test(BasicOperation.class, x, y);
       test(ExtendedOperation.class, x, y);
}

NO.38、注解优先于命名模式
        命名模式,有些通过工具或者框架进行处理。如Junit要求使用test作为测试方法名称的开头。
       注解适用于特定的场景,关于它的好处不用多说。

       注解实现示例:

 

//专用于普通测试注解,该注解只适用于静态的无参方法,
//如果使用地方不正确由注解工具自己把握
@Retention(RetentionPolicy.RUNTIME)//注解信息保留到运行时,这样工具可以使用
@Target(ElementType.METHOD)//只适用于方法
public @interface Test {}

//专用于异常测试的注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
       //测试方法可能抛出多个异常
       Class<? extends Exception>[] value();
}
下面应用上面定义的注解:
public class Sample {
       @Test
       public static void m1() {} // 测试应该通过
       public static void m2() {}
       @Test
       public static void m3() { // 测试应该失败
              throw new RuntimeException("Boom");
       }
       public static void m4() {}
       @Test//不应该使用在这里,但应该由注解工具自己处理这种不当的使用
       public void m5() {} // 错误使用: 非静态方法
       public static void m6() {}
       @Test
       public static void m7() { // 测试应该失败
              throw new RuntimeException("Crash");
       }
       public static void m8() {}
}
public class Sample2 {
       @ExceptionTest(ArithmeticException.class)
       public static void m1() { // 测试应该要通过,因为抛出了算术异常
              int i = 0;
              i = i / i;
       }
       @ExceptionTest(ArithmeticException.class)
       public static void m2() { // 测试应该不通过,因为抛出的异常为数组越界异常
              int[] a = new int[0];
              int i = a[1];
              System.out.println(i);
       }
       @ExceptionTest(ArithmeticException.class)
       public static void m3() {
       } // 测试应该不通过,因为没有抛也异常

       // 可能抛出多个异常,使用{}括起来,如果是单个可以省略
       @ExceptionTest( { IndexOutOfBoundsException.class,
                     NullPointerException.class })
       public static void doublyBad() {
              List<String> list = new ArrayList<String>();
              //这里会抛出空指针错误,测试应该会通过
              list.addAll(5, null);
       }
}

注解的切面应用:
public class RunTests {
       public static void main(String[] args) throws Exception {
              int tests = 0;//需要测试的数量
              int passed = 0;//测试通过的数量
              //加载被注解的类,即被测试的类
              Class<?> testClass = Class.forName(args[0]);
              //遍历测试类的所有方法
              for (Method m : testClass.getDeclaredMethods()) {
                    
                     //Test注解实现工具
                     if (m.isAnnotationPresent(Test.class)) {
                            tests++;
                            try {
                                   m.invoke(null);//没有参数,表示调用的是静态方法
                                   passed++;//如果方法调用成功则表示测试通过
                            }//表示测试方法本身抛出了异常
                            catch (InvocationTargetException wrappedExc) {
                                   Throwable exc = wrappedExc.getCause();
                                   //打印测试方法抛出的异常信息
                                   System.out.println(m + " failed: " + exc);
                            }//如果抛异常表示注解使用错误,不应使用在非静态或带参数的方式上
                            catch (Exception exc) {
                                   //打印测试未通过的方法信息
                                   System.out.println("使用@Test注解错误的方法 : " + m);
                            }
                     }
                    
                     // ExceptionTest注解实现工具
                     if (m.isAnnotationPresent(ExceptionTest.class)) {
                            tests++;
                            try {
                                   m.invoke(null);
                                   //如果注解工具运行到这里,则测试方法未抛出异常,但属于测试未通过
                                   System.out.printf("Test %s failed: no exception%n", m);
                            } catch (Throwable wrappedExc) {
                                   //获取异常根源
                                   Throwable exc = wrappedExc.getCause();
                                   //取出注解的值
                                   Class<? extends Exception>[] excTypes = m.getAnnotation(
                                                 ExceptionTest.class).value();
                                   int oldPassed = passed;
                                   //将根源异常与注解值对比
                                   for (Class<? extends Exception> excType : excTypes) {
                                          //如果测试方法抛出的异常与注解中预期的异常匹配则表示测试通过
                                          if (excType.isInstance(exc)) {//使用动态的instance of
                                                 passed++;
                                                 break;
                                          }
                                   }
                                   //打印测试没有通过的方法信息
                                   if (passed == oldPassed)
                                          System.out.printf("Test %s failed: %s %n", m, exc);
                            }
                     }
              }
              //打印最终测试结果,通过了多少个,失败了多少个
              System.out.printf("Passed: %d, Failed: %d%n", passed, tests - passed);
       }
}

NO.39、坚持使用Override注解
        意思是如有继承、实现时,尽量明确写上Override,增加可读性且避免出现重载现象。例如当你打算重写一个方法时,有可能写成重载,这时如果@Override注解就可以防止出现这个问题:
       public boolean equals (Foo that){…}
       当你应当把他们编写成如下时:
       public Boolean equals(Object that)
       这也是合法的,但是类Foo从Object继承了equals实现,最终成了重载,而原本是重写的,这时我们可以使用@Override在重写的方法前,这样如果在没有重写的情况下,编译器则会提示我们。
NO.40、用标记接口定义类型
       标记接口是不包含方法声明的接口,只是指明一个类实现了具有某种属性的接口。如Serializable,表明它的实例可被写到ObjectOutputStream中(序列化),注意:标记接口是用来定义类型的,以上的标记注解是用来辅助分析类元素信息的。
NO.41、检查参数的有效性
       方法和构造器的执行通常对传递给它们的参数值都有些限制,因此应在方法和构造器体前对需要检验的参数的进行有效性检查。
NO.42、必要时进行保护性拷贝
       如上,所谓保护性拷贝就是在拷贝前进行有效性验证。如果类具有从客户端得到或者返回到客户端的可变组件,类就必须保护性地拷贝这些组件。如此可确保数据输入输出的准确性。
NO.43、慎设计方法签名
       定义方法名、参数类型、参数列表的一些准则,不用多说。
NO.44、慎用重载
       通常对多个具有相同参数数目的方法来说,应尽量避免重载。当然涉及构造器时,要遵循这条建议也许是不可能的,因此至少应避免这样的情形:同一组参数只需经过类型转换就可被传递给不同的重载方法,要坚决抵制这样的重载。

 

public class CollectionClassifier {
       public static String classify(Set<?> s) {
              return "Set";
       }

       public static String classify(List<?> lst) {
              return "List";
       }

       public static String classify(Collection<?> c) {
              return "Unknown Collection";
       }

       public static void main(String[] args) {
              Collection<?>[] collections = { new HashSet<String>(),
                            new ArrayList<BigInteger>(), new HashMap<String, String>().values() };
             
              for (Collection<?> c : collections)
                     System.out.println(classify(c));
       }
}

public static String classify(Collection<?> c) {
       return c instanceof Set ?"Set": c instanceof List ?"List"
                     :Unknown Collection";
}

        上面程序三次打印“Unknown Collection”,而没有打印出“Set”与“List”,方法被覆盖了。
NO.45、慎用可变参数
       可变数组方法接受0个或者多个指定类型的参数。可变参数机制通过先创建一个数组,数组大小为在调用位置所传递的参数数量,然后将参数值传到数组中,最后将数组传递给方法,因此可变参数会影响性能,不可滥用。
       设计可变参数时,如果至少要传递一个参数,则最好将这个参数单独做为第一个参数,而不是都做成可变参数后在方法里进行参数检测

 

//如果前面不对参数进行有效性检测,又如果运行时没有传递参数,则运行时出错
static int min(int firstArg, int... remainingArgs) {
       int min = firstArg;
       for (int arg : remainingArgs)
              if (arg < min)
                     min = arg;
       return min;
} 

NO.46、返回零长度的数组或者集合,而不是null
       习惯问题,对于一个返回null而不是零长度数组或者集合方法,几乎每次用到该方法时都需要额外处理是否为null,这样做很容易出错,因为缩写客户端程序的程序员可能会忘记写这种专门的代码来处理null返回值。
       return cheesesInStock.toArray(EMPTY_CHEESE_ARRAY);//数组
       return new ArrayList<Cheese>(cheesesInStock);//集合
NO.47、为所有导出的API元素编写文档注释
       ……
NO.48、将局部变量的作用域最小化
       的确如此。可以增强代码的可读性和可维护性,并降低出错的可能性。
       要使用局部变量的作用域最小化:
       1、第一次使用它的地方才声明,不要过早的声明。
       2、使方法小而集中
NO.49、for-each循环优先于传统的for循环
        早就知道的吧,pass。
NO.50、了解和使用类库
       不要重新发明轮子,善用JDK。
NO.51、如需精确的答案,请避免使用float和double
       这两者不完全精确,如果性能非常关键,请使用int和long,如果数值范围没有超过9位十进制数字,就可使用int;如果不超过18位数字,就可使用long,如果数字可能超过18位数字,就必须使用BigDecimal。
NO.52、基本类型优先于装箱或包装类型
       反复的拆箱、装箱耗费性能。

 

public int compare(Integer first, Integer second) {
int f = first; // 自动拆箱
int s = second; // 自动拆箱
return f < s ? -1 : (f == s ? 0 : 1); // 按基本类型比较
}

NO.53、如果其他类型更适合,则尽量避免使用字符串
       字符串不适合代替其他的值类型,应尽量将它们转换为确切的类型,当明则明。
       错误地用字符串来代替的类型包括基本类型、枚举类型和聚集类型,使用不当,字符串会比其他类型更加笨拙、更不灵活、速度慢,也更容易出错。
NO.54、当心字符串连接的性能
       个人觉得需根据情况决定是否+或StringBuilder,杀鸡不可用牛刀。
       在JDK1.5后:String s = "a" + "b" + "c"; 在编译时,编译器会自动引入StringBuilder连接。但不适合循环类的操作,循环类的应自写StringBuilder来处理连接。
       因此多数情况下,尽可能将字符串连接集中在一个表达式里描述(否则会产生多个StringBuilder),然后让编译器来替我们使用 StringBuffer/StringBuilder ,只有当字符串连接不得不涉及到多条语句时,才有必要显式使用 StringBuffer/StringBuilder。
NO.55、通过接口引用对象(针对接口编程)
       OO原则,不多说。
NO.56、接口优先于反射机制(使用接口类型引用反射创建实例)
       反射是一种功能强大的机制,对于特定的复杂系统编程任务,是非常必要的,虽然也有一些缺点。如有可能应仅仅使用反射机制来实例化对象,而访问对象则使用编译时已知的某个接口或超类。
NO.57、谨慎使用本地方法
       Java Native Interface(JNI)允许Java可调用本地方法(C或者C++来编写的特殊方法)。
       本地方法主要有三种用途:
       1、访问特定于平台的机制
       2、访问遗留代码库的能力
       3、可通过本地语言,编写应用程序中注重性能的部分,以提高系统的性能
       极少数情况下会使用本地方法提高性能。如必须要使用本地方法访问底层资源或遗留代码,要尽可能少的使用本地代码。
NO.58、谨慎地进行优化(预先在设计过程中考虑性能问题)
       不用多说,不成熟的优化只会更糟,优化的弊小于利。优化格言:
       1、很多计算上的过失都被归咎于没有必要达到的效率,而不是任何其他原因
       2、不去计较效率上的小小得失,在97%的情况下,不成熟优化才是一切问题根源。
       3、在优化方面,应遵循:
       规则1:不要进行优化。
       规则2(仅针对专家):还是不要进行优化,也就是说在你还没有绝对清晰的未优化方案之前,请不要优化。
       不要因为性能而牺牲合理的结构,要努力编写好的程序而不是费力的去编写快的程序,速度自然会随之而来。好的程序体现了信息隐藏原则:只要有可能,它们就会设计决策集中在单个模块里,因此可改变单个的决策而不会影响到系统的其他部分。
       在设计系统时,特别是在设计API、线路层协议和永久数据库格式的时候(模块之间的交互与模块与外界的交互一旦定下来后是不可能更改的),一定要考虑性能的因素。
       穿插些本外关于性能的言论:
       先要把焦点放在设计、数据结构和算法身上,不要因要求提高程序执行速度,而放弃了良好、可靠的设计,转而是追求不可能达到或甚小的性能改良。
       产生运行快的代码的一个规则是,只优化必要的部分。花费时间将代码优化,却未能给程序带来实质性的性能影响,就是在浪费时间。如果80%-90%的程序执行的时间花费在10%-20%的代码上面(80-20法则),那你最好找出这需要改善的10%-20%代码然后进行优化。
NO.59、遵守普遍接受的命名惯例
        Pass…
NO.60、只针对异常的情况才使用异常
       在现代的JVM实例上,基本异常的模式比标准模式要慢得多,异常只用于异常的情况下,它们永远不应该用于正常的控制流。

 

//基本异常模式
try{
       int i = 0;
       while(true)
         range[i++].climb();
}catch(ArrayIndexOutOfBoundsException e){}
for(int i=0;i<a.length;i++){  
     a[i].f();  
} 


        基本异常会引起:
       1、创建、抛出和捕获异常的开销是很昂贵的。因为它的初衷是用于不正常的情形,少有jvm会它进行性能优化
       2、把代码放在try-catch中会阻止jvm实现本来可能要执行的某些特定的优化
       3、有些现代的jvm对循环进行优化,不会出现冗余的检查

       补充:
       1、finally块中出现异常没有捕获或捕获后重新抛出会覆盖try、catch中出现的异常
       2、不要在try块中调用return、break或continue,万一无法避免,一定要确保finally的存在不会改变函数的返回值,如果有返回值最好在try与finally外返回。
       3、不要将try/catch放在循环内,那样会减慢代码的执行速度。
       4、如果构造器调用的代码需要抛出异常,就不要在构造器处理它,而是直接在构造器声明上throws出来,这样更简洁与安全。
NO.61、对可恢复的情况使用受检异常,对编程错误使用运用时异常
       受检异常(checked exception)、运行时异常(run-time exception)和错误(error)。关于什么时候适合使用哪种异常,据情况而定。
       受检异常:FileNotFoundException,需检查或捕获后返回提示给客户或需要恢复异常
       运行时异常:ArrayIndexOutOfBoundsException,运行错误,不需要、也不应被捕获
NO.62、避免不必要地使用受检异常
       受检异常(检查后抛出的异常)与运行时异常不一样,它强迫程序员处理异常的条件,大大增强了可靠性。
       过分使用受检异常不合适。如果方法抛出一个或者多个受检异常,调用都就必须在一个或多个catch块中处理,或者将它们抛出并传播出去,因此尽量条件判断后抛出更有价值的异常。

 

}catch(TheCheckedException e){
       throw new AssertionError();// 断言不会发生异常
}
或
}catch(TheCheckedException e){// 哦,失败了,不能恢复
       e.printStackTrace();
       System.exit(1);
}
// TheCheckedException设计成运行时异常会更好些,throw出来更合适

NO.63、优先使用标准异常
        那就是JAVA库提供的异常了……
NO.64、在无法避免底层异常时,抛出与系统直接相关的异常
       处理底层异常最好的方法首选是阻止底层异常的发生,如不能阻止或处理底层异常时,一般使用异常转换,抛出一个可以按照高层抽象进行解释的异常。

 

//异常链
    try{  
      //use lowlevel abstraction to do our bidding  
      ...  
    }catch(LowerLevelException e){  
      throw new HigherLevelException(...);  
    }  
    //异常转义
    try{  
      //use lower-level abstraction to do our bindding  
      ...  
    }catch(LowerLevelException e){  
      throw new HigherLevelException(e);  
    }  

NO.65、每个方法抛出的异常都要有文档描述
……
NO.66、异常信息中要包含足够详细的异常细节消息
……
NO.67、努力使失败保持原子性
        失败时方便异常恢复,这个我觉得有很多处理方式,不必效仿书本上所言。
NO.68、不要忽略异常

 

// 忽略异常块,强烈反对
try {
……
} catch (SomeException e) {}

        空的catch块会使异常达不到应用目的。至少catch块应包含一条说明,解释为什么可以忽略。
NO.69、    同步访问共享的可变数据
       同步不仅仅是简单的互斥,同步 = 原子性(阻止不一致) + 可见性(线程都看到由同一个锁保护的之前所有的修改结果)
       synchronized就是同步的代名词,它具有原子性与可见性。而volatile(线程间通信,互通有无让彼此知道状态,不排斥)只具有可见性,但不具有原子性。
       在读或写原子数据的时候(比如操作long、double、boolean),同样需要使用同步

 

public class StopThread {
    private static boolean stopRequested;
    private static synchronized void requestStop() {
       stopRequested = true;
    }
    private static synchronized boolean stopRequested() {
       return stopRequested;
    }
    public static void main(String[] args) throws InterruptedException {
       Thread backgroundThread = new Thread(new Runnable() {
           public void run() {
              int i = 0;
              while (!stopRequested())
                  i++;
           }
       });
       backgroundThread.start();
       TimeUnit.SECONDS.sleep(1);
       requestStop(); //停不下来,不生效
    }
}

        写方法(requestStop)和读方法(stopRequested)都被同步了,但是还不够,这样的同步只是为了通信效果(即可见性),而不是为了互斥访问(即原子性)。
       用下面代码替换即可:

 

public class StopThread {
    private static volatile boolean stopRequested;
    public static void main(String[] args) throws InterruptedException {
       Thread backgroundThread = new Thread(new Runnable() {
           public void run() {
              int i = 0;
              while (!stopRequested)
                  i++;
           }
       });
       backgroundThread.start();
       TimeUnit.SECONDS.sleep(1);
       stopRequested = true;
    }
}

       虽然volatile修饰符不具有互斥访问的特性,但它可以保证任何一个线程在读取该域的时候都将看到最近刚刚被其他线程写入的值。如果只是需要线程之间的交互通信,而不需要互斥,volatile修饰就是一种可以接受的同步形式。

 

private static volatile int nextSerialNumber = 0;
public static int generateSerialNumber() {
    return nextSerialNumber++; //不安全,存在中间处理过程
}

       使用原子类:

 

private static final AtomicLong nextSerialNum = new AtomicLong();
public static long generateSerialNumber() {
    return nextSerialNum.getAndIncrement();
}

NO.70、避免过多同步
       当然……
       同步块中,避免调用外来方法干扰执行的目标对象,最好封闭完成事务。
       同步块中,尽量不要处理长时事务。
       不要过多同步,影响性能
NO.71、task(工作单元)和executor(执行机制)优先于线程(工作单元 + 执行机制)
       ExecutorService、ScheduledExecutorService……
NO.72、并发工具优先于wait和notify
       永远不要在循环的外面调用wait-----
       使用wait方法的标准模式:

 

synchronized(obj){
       while(<等待条件>){
              obj.wait();
}
… // 条件满足后开始处理
}
而不能是这样:
if(<等待条件>){
       obj.wait();
}

       当线程会进入锁对象的等待池,在被唤醒后不会马上进入就绪状态,而是进入锁对象的锁池,只有再一次获取锁后,才能进入到就绪状态,也有可能就在它再一次获取锁前,等待条件被另一线程改变了,或者是在等待条件还根本还未破坏时另一线程意外或恶意的调用了notify或notifyAll。
NO.73、    线程安全性的文档化
       尽量别使用公有对象来作为锁对象,因为这样外界可能意外或者故意的霸占锁,造成拒绝服务,所以这该使用私有锁对象来代替同步方法(非静态的同步方法的锁就是this,但这个对象在外面可以访问到,所以避免使用):
private final Object lock = new Object();//注意最好声明成final的

       线程安全性级别:
       1、不可变类——这个类的实例是不可变的,所以不需外部的同步,如String、Long和BigInteger。
       2、无条件的线程安全——这个类的实例是可变的,但是这个类有足够的内部同步,所以它的实例可被并发使用,无需任何外部同步。如Random和ConcurrentHashMap。
       3、有条件的线程安全——除有些方法为进行安全的并发使用而需要外部同步外,这种线程安全级别与无条件的线程安全相同。如Collections.synchronized包装返回的集合,它们的迭代器(iterator)要求同步,否则在迭代期间被其他线程所修改。下面是源码:

 

    public Iterator<E> iterator() {
        return c.iterator(); // Must be manually synched by user!
   }

       4、非线程安全——这个类的实例是可变的。为了并发使用它们,客户必须利用自己选择的外部同步包围每个方法调用。如集合ArrayList、HashMap。
       5、线程对立的——这个类不能安全地被多个线程并发使用,即使所有的方法调用都被外部同步包围。线程对立的根源通常在于,没有同步地修改静态数据。没有人会有意编写一个线程对立的类;这种类是因为没有考虑到并发性而产生后果。幸运的是,在Java平台类库中,这样的类很少,System.runFinalizersOnExit()是这样的,但已废除了。
NO.74、慎用延迟初始化
       延迟初始化降低了初始化类或者创建实例的开销,却增加了访问被延迟初始化的域的开销。如果域只在类的实例部分被访问,并且初始化这个域的开锁很高,可能就值得进行延迟初始化。
       大多数的域应该正常地进行初始化,而不是延迟初始化。如果为了达到性能目标,可以使用相应的延迟初始化方法。对于实例域,使用双重检查模式;对于静态域,则使用类延迟加载。对于可以接受重复初始化的实例域,也可考虑使用单重检查模式。下面是上面完全实例代码:

 

class FieldType {}
// 各种初始化模式
public class Initialization {
    // 正常初始化
    private final FieldType field1 = computeFieldValue();
    private static FieldType computeFieldValue() {
       return new FieldType();
    }

    // 延迟初始化模式 - 同步访问
    private FieldType field2;
    synchronized FieldType getField2() {
       if (field2 == null)
           field2 = computeFieldValue();
       return field2;
    }

    // 对静态域使用类延迟初始化
    private static class FieldHolder {
       static final FieldType field = computeFieldValue();
    }
    static FieldType getField3() {
       return FieldHolder.field;
    }

    // 对实例域进行双重检测延迟初始化
    private volatile FieldType field4;
    FieldType getField4() {
       FieldType result = field4;
       if (result == null) { // First check (no locking)
           synchronized (this) {
              result = field4;
              if (result == null) // Second check (with locking)
                  field4 = result = computeFieldValue();
           }
       }
       return result;
    }

    // 单重检查 - 会引起重复初始化
    private volatile FieldType field5;
    private FieldType getField5() {
       FieldType result = field5;
       if (result == null)
           field5 = result = computeFieldValue();
       return result;
    }
}

NO.75、不依赖于线程调度器
       任何依赖于线程调度器来达到正确性或者性能要求的程序,很有可能是不可移植的。
       要编写健壮的、响应良好的、可移植的多线程程序应用程序,最好的办法确保可运行线程的平均数量不明显多于处理器的数量。
       线程不应该一直处于忙等的状态,即反复地检查一个共享对象,以等待某些事情的发生。
       不要人为地调整线程的优先级,线程优先级是Java平台上最不可移值的特征了。
NO.76、避免使用线程组ThreadGroup
       线程组初衷是作为安全隔离一些小程序的机制,但从来没有真正履行这个承诺,其安全价值已经差到根本不能在Java安全模型的标准工作中提及的地步。
       除了安全性外,它们允许你同时把Thread的某些基本功能应用到一组线程上,但有时已被废弃,剩下的也很少使用,因为它们有时并不准确。
       总之,线程级并没有提供太多有用的功能,而且它们提供的许多的功能都有缺陷的。我们最好把线程组看作是一个不成功的试验,你可以忽略他们,就当不存在一样。如果你正在设计一个类需要处理线程的逻辑组,或许应该使用线程池executor。
NO.77、谨慎地实现Serializable接口
       其实也没书中说的那么大的问题,有需要时使用,目前有很多开源组件,不需要实现该接口即可序列化。
NO.78、考虑使用自定义的序列化形式
……
NO.79、保护性地编写readObject方法
……
NO.80、对于实例控制,枚举类型优先于readResolve
……
NO.81、考虑用序列化代理代替序列化实例
……

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值