用静态工厂方法代替构造器

类可以提供一个静态工厂方法(static factory method) :一个返回类的实例的静态方法。

下面是来自Boolean的简单示例:

public static Boolean valueOf(boolean b) {return b ? Boolean.TRUE : Boolean.FALSE;
}

注意这与工厂方法设计模式不同

静态工厂方法与构造器不同的第一大优势有:

  • 它们有名称。
    产生的客户端代码也更容易阅读。当一个类需要多个带有相同签名的构造器时,就用静态工厂方法代替构造器,并仔细地选择名称以便突出静态工厂方法之间的区别。
  • 不必每次调用它们的时候都创建一个新对象。
    这使得不可变类可以使用预先构建好的实例,或将构建好的实例缓存起来,可以重复利用,从而避免创建不必要的对象。Boolean.valueOf(boolean)方法说明了这项技术:它从来不创建对象。这种方法类似于享元(Flyweight)模式。如果程序经常请求创建相同的对象,并且创建对象的代价很高,则这项技术可以极大地提高性能。
    静态工厂方法还能够为重复的调用返回相同对象,这样能控制在某个时刻哪些实例应该存在。
  • 它们可以返回原类型的任何子类对象。
    这样在选择返回对象的类时就有了更大的灵活性。
  • 所返回的对象的类可以随着每次调用而发生变化,这取决于静态工厂方法的参数值。
    只要是已声明的返回类型的子类型,都是允许的。
  • 方法返回的对象所属的类,在编写包含该静态工厂方法的类时可以不存在。

静态工厂方法的缺点有:

  • 类如果不含有公有的或受保护的构造器,就不能被子类化。
  • 程序员很难发现它们。
    它们不像构造器那样在API文档中明确标识出来。

下面是静态工厂方法的一些惯用名称:

  • from——类型转换方法,它只有单个参数,返回该类型的一个相对应的实例:Date d = Date.from(instant);
  • of——聚合方法,带有多个参数,返回该类型的一个实例,把它们合并起来:Set<Rank> faceCards = EnumSet.of(JACK,QUEEN,KING);
  • valueOf——比fromof更烦琐的一种替代方法:BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
  • instancegetInstance——返回的实例是通过方法的参数来描述的,但不能说与参数具有同样的值:StackWalker luke = StackWalker.getInstance(options);
  • createnewInstance——每次调用都能返回一个新的实例:Object newArray = Array.newInstance(classObject,arrayLen);
  • getType——像getInstance一样,但是在工厂方法处于不同的类中的时候使用。Type表示工厂方法所返回的对象类型:FileStore fs = Files.getFileStore(path);
  • newType——像newInstance一样,但是在工厂方法处于不同的类种的时候使用。Type表示工厂方法所返回的对象类型:BufferedReader bf = Files.newBufferedReader(path);
  • type——getType和newType的简版:List<Complaint> litany = Collections.list(legacyLitany);

总之,静态工厂方法和公有构造器都各有用处,我们需要理解它们各自的长处。切忌第一反应就是提供公有的构造器,应该考虑静态工厂。

遇到多个构造器参数时要考虑使用构建器

静态工厂和构造器有个共同的局限性:它们都不能很好地扩展到大量的可选参数。

比如用一个类表示包装食品外面显示的营养成分标签。这些标签中有几个属性是必需的:每份的含量、每罐的含量以及每份的卡路里。这有超过20多个可选属性:总脂肪量、胆固醇等等。

对于这样的类,应该用哪种构造器或静态工厂来编写呢?通常想到的是重叠构造器模式,在这种模式下,提供的第一个构造器只有必要的参数,第二个构造器有一个可选参数,第三个构造器有两个可选参数,依此类推,最后一个构造器包含所有的可选参数。 这样当可选参数很可观的时候,代码会很难编写,同时难以阅读。

还有第二种代替方法,JavaBeans模式,在这种模式下,先调用一个无参构造器来创建对象,然后再调用setter方法来设置每个必要的参数,以及每个相关的可选参数:

这个我要通过代码来描述一下,因为我在工作中遇到了这种情况。

package com.java.effective.createobject;/*** @Author: Yinjingwei* @Date: 2019/5/22/022 23:09* @Description:*/
public class NutritionFacts {/*** 每份的含量*/private int servingSize = -1;//必要参数;非默认值/*** 每罐的含量(每罐含有多少份)*/private int servings = -1;//必要参数;非默认值/*** 每份的卡路里*/private int calories = 0;/*** 总脂肪量*/private int fat = 0;/*** 钠*/private int sodium = 0;/*** 含糖量*/private int carbohydrate = 0;public NutritionFacts(){}public void setServingSize(int servingSize) {this.servingSize = servingSize;}public void setServings(int servings) {this.servings = servings;}public void setCalories(int calories) {this.calories = calories;}public void setFat(int fat) {this.fat = fat;}public void setSodium(int sodium) {this.sodium = sodium;}public void setCarbohydrate(int carbohydrate) {this.carbohydrate = carbohydrate;}
}

这种模式弥补了重叠构造器模式的不足,创建实例很容易,可读性也OK:

NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);

但是,该模式有很严重的缺点。因为构造过程被分到了几个调用中,在构造过程中JavaBean可能处于不一致的状态。另外,该模式不可能把类做成不可变的。

幸运的是,有第三种替代方案——建造者(Builder)模式。它不直接生成想要的对象,而是让客户端利用所有必要的参数调用构造器(或静态工厂),得到一个builder对象。然后客户端在builder对象上调用类似于setter方法来设置每个相关的可选参数。最后调用无参的build方法来生成通常不可变的对象。这个builder通常是它构建的类的静态成员类

package com.java.effective.createobject;/*** @Author: Yinjingwei* @Date: 2019/5/22/022 23:09* @Description:*/
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) {servings = builder.servings;servingSize = builder.servingSize;calories = builder.calories;fat = builder.fat;sodium = builder.sodium;carbohydrate = builder.carbohydrate;}public static class Builder {//必要参数 加上final 使得必须要在构造函数中赋值private final int servingSize;private final int servings;//可选参数private int calories = 0;private int fat = 0;private int sodium = 0;private int carbohydrate = 0;public Builder(int servingSize,int servings) {this.servings = servings;this.servingSize = servingSize;}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);}}
}

注意到NutritionFacts本身是不可变的。

客户端代码:

 NutritionFacts cocaCola = new Builder(240,8).calories(100).sodium(35).carbohydrate(27).build();

Builder模式也适用于类层次结构。

public abstract class Pizza {/*** 表示各种各样的披萨*/public enum Topping {HAM, MUSHROOM, ONION, PEPPER, SAUSAGE}final Set<Topping> toppings;abstract static class Builder<T extends Builder<T>> {EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);public T addTopping(Topping topping) {toppings.add(Objects.requireNonNull(topping));return self();}abstract Pizza build();//子类必须覆盖该方法返回 thisprotected abstract T self();}Pizza(Builder<?> builder) {toppings = builder.toppings.clone();}
}

然后又两个具体类型的披萨

public class NyPizza extends Pizza {//尺寸属性public enum Size {SMALL, MEDIUM, LARGE}private final Size size;public static class Builder extends Pizza.Builder<Builder> {private final Size size;public Builder(Size size) {this.size = Objects.requireNonNull(size);}@Overridepublic NyPizza build() {return new NyPizza(this);}@Overrideprotected Builder self() {return this;}}private NyPizza(Builder builder) {super(builder);size = builder.size;}
}public class Calzone extends Pizza{//酱汁单独弄出来还是放里面private final boolean sauceInsize;public static class Builder extends Pizza.Builder<Builder> {private boolean sauceInside = false;public Builder sauceInside() {sauceInside = true;return this;}@Overridepublic Calzone build() {return new Calzone(this);}@Overrideprotected Builder self() {return this;}}private Calzone(Builder builder) {super(builder);sauceInsize = builder.sauceInside;}
}

每个子类的构建器中的build方法,都声明返回正确的子类。子类方法声明返回超类中声明的返回类型的子类型,这被称为协变返回类型(covariant return type)

这些层次化构建器的客户端代码本质上与简单的构建器一样:

NyPizza pizza = new NyPizza.Builder(NyPizza.Size.SMALL).addTopping(Topping.SAUSAGE).addTopping(Topping.ONION).build();
Calzone calzone = new Calzone.Builder().addTopping(Topping.HAM).sauceInside().build();

如果类的构造器或者静态工厂中具有多个(超过5个吧)参数,设计这种类时,Builder模式就是一个不错的选择

另外,lombok插件的@Builder注解可以了解一下

用私有构造器或枚举类型强化Singleton属性

**Singleton(单例)**是指仅仅被实例化一次的类。通常用来代表一个无状态(也就是无属性)的对象。

实现Singleton有两种常见的方法。这两种方法都要保持构造器为私有的,并导出公有的静态成员,使得客户端能访问该类的唯一实例。

第一种方法中,公有静态成员是个final属性:

public class Elvis {public static final Elvis INSTANCE = new Elvis();private Elvis() {...}public void leaveTheBuilding(){...}
}

一旦Elvis类被实例化,将只会存在一个Elvis实例。但是可以通过反射机制调用私有构造器。如果要防止这种攻击,可以修改构造器,让它在被要求创建第二个实例的是抛出异常。

第二种方法中,公有的静态成员是个静态工厂方法:

public class Elvis {private static final Elvis INSTANCE = new Elvis();private Elvis() {...}public static Elvis getInstance() {return INSTANCE;}public void leaveTheBuilding(){...}
}

公有属性实现的单例的主要优势在于,API很清楚的表明了这个类是一个单例,第二个优势在于它更简单。

静态工厂方法的优势之一在于,它提供了灵活性:在不改变API的前提下,我们可以改变该类是否为单例的想法。第二个优势在于,如果需要,可以编写一个泛型的单例工厂。最后可以通过方法引用作为Supplier,比如Elvis::instance就是一个Supplier<Elvis>。除非满足以上任意一种优势,否则优先考虑第一种方法。

为了将利用上述方法实现的单例类变成可序列化的,仅仅在声明中加上implements Serializable是不够的。为了维护并保证单例,必须声明所有实例都是transient,并提供一个readResolve方法。否则,每次反序列化一个序列化的实例时,都会创建一个新的实例。

private Object readResolve(){return INSTANCE;
}

实现单例模式的第三种方法是声明一个包含单个元素的枚举类型:

public enum Elvis {INSTANCE;public void leaveTheBuilding(){...}
}

单元素的枚举类型经常是实现单例的最佳方法。

通过私有构造器强化不可实例化的能力

我们编写的工具类不想被别人实例化,该怎么做呢?
由于只有当类不包含显示的构造器时,编译器才会生成缺省的构造器。因此只要让这个类包含一个私有构造器,它就不能被实例化。

public class UtilityClass {private UtilityClass() {}//....
}

这种习惯用法也有副作用,它使得一个类不能被子类化。所有的构造器都必须显示或隐式地调用超类构造器,在这种情况下,子类就没有可访问的超类构造器可调用了。

优先考虑依赖注入来引用资源

有许多类会依赖一个或多个底层的资源。例如,拼写检查器需要依赖词典。因此,像下面这样把类实现为静态工具类的做法并不少见:

public class SpellChecker {private static final Lexicon dictionary = ...;private SpellChecker(){} //不可实例化public boolean isValid(String word){...}public List<String> suggestions(String typo){...}
}

同样,也将这些类实现为单例:

public class SpellChecker {private static final Lexicon dictionary = ...;private SpellChecker(...){}public static INSTANCE = new SpellChecker(...);public boolean isValid(String word){...}public List<String> suggestions(String typo){...}
}

以上两种方法都不理想,因为它们都是假定只有一本词典可用。但是可能需要用特殊的词典进行测试。假定只有一本词典就能满足所有需求是不可能的。

由此可见,静态工具类和单例类不适合于需要引用底层资源的类。因为一般需要支持多个底层资源实例。

满足该需求的最简单的模式是,当创建一个新的实例时,就将资源传到构造器中。这就是依赖注入的一种形式:
词典是拼写检查器的一个依赖,当创建拼写检查器时就将词典注入其中。

public class SpellChecker {private static final Lexicon dictionary;public SpellChecker(Lexicon dictionary){this.dictionary = Objects.requireNonNull(dictionary);}public boolean isValid(String word){...}public List<String> suggestions(String typo){...}
}

将不同的词典注入(传入)到构造函数中,就能实例化出不同的拼写检查器。

这种方式极大地提升了类的灵活性、可重用性和可测试性。

避免创建不必要的对象

一般来说,最好能重用单个对象,而不是在每次需要的时候就创建一个相同功能的新对象。
如果对象是不可变的,它就始终可以被重用。

String s = new String("hello");该语句在每次诶执行的时候都会创建一个新的String实例。如果这种用法是在一个循环中,或在一个被频繁调用的方法中,就会创建成千上万个不必要的String实例。

改进后的版本:String s = "hello";

对于同时提供了静态工厂方法和构造器的不可变对象类,通常优先使用静态工厂而不是构造器,以避免创建不必要的对象。例如,静态工厂方法Boolean.valueOf(String)几乎总是优于构造器Boolean(String)。构造器在每次被调用的时候都会创建一个新的对象,而静态工厂方法则从来不要求这么做,实际上也不会这么做。

有些对象创建的成本比其他对象要高得多。如果重复地需要这类昂贵的对象,建议将它缓存下来重用。

遗憾的是,在创建这种对象的时候,并非总是那么显而易见。比如想要编写一个方法,用来测试一个字符串是否为一个有效的罗马数字:

static boolean isRomanNumeral(String s) {return s.matches("<复杂的正则表达式>");
}

这样实现的问题在于它依赖String.matches方法,但该方法并不适合在注重重用性能的情形中重复使用。它在内部会创建一个Pattern实例,却只用了一次,之后就进行垃圾回收了。而创建该实例的成本很高。为了提升性能,应该显示地将正则表达式编译成一个Pattern实例(不可变),让它成为类初始化的一部分,并将它缓存起来。每次调用判断方法时就重用同一个实例:

private static final Pattern ROMAN = Pattern.compile("<复杂的正则表达式>");
static boolean isRomanNumeral(String s) {return ROMAN.machers(s).matches();
}

如果一个对象是不可变的,那么它显然能够被安全地重用,但其他情形并不总是这么明显。

考虑适配器的情形,有时也叫视图(view)。它把功能委托给一个支撑对象(backing object),从而为支撑对象提供一个可以替代的接口。由于适配器除了支撑对象之外,没有其他的状态信息,所以针对某个给定对象的特定适配器而言,它不需要创建多个适配器实例。

例如,Map接口的keySet方法返回该对象的Set视图,其中包含该对象中所有的键。好像每次调用keySet都应该创建一个新的Set实例,但是,对于一个给定的Map对象,实际上每次调用keySet都返回同样的Set实例。虽然被返回的Set实例一般是可改变的,但是所有返回的对象哎功能上都是等同的:当其中一个返回对象发生变化时,所有其他的返回对象也要发生变化,因为它们是由同一个Map实例支撑的。

这是内部类的一个体现:内部类与创建它的外部类实例有联系。从下面的代码可以体现出来,最后输出set1,能输出3个键,说明是与外部类有联系的。

HashMap<String,String> map = new HashMap<>();
map.put("A","a");
map.put("B","b");Set<String> set1 =  map.keySet();
Set<String> set2 = map.keySet();
System.out.println(set1 == set2);//true 其实内部返回的是同一个set对象
System.out.println(set1);
map.put("C","c");
System.out.println(set1);//A,B,C

另外一种创建多余对象的方法,称作自动装箱。自动装箱会有所消耗,如果在频繁需要用到自动装箱的情况下,要优先使用基本类型而不是装箱基本类型,要当心无意识的自动装箱。

消除过期的对象引用

看下面这个简单实的栈实现的例子:

package com.java.effective.createobject;import java.util.Arrays;
import java.util.EmptyStackException;/*** @Author: Yinjingwei* @Date: 2019/5/29/029 21:38* @Description:*/
class Stack {private Object[] elements;private int size = 0;private static final int DEFAULT_INITIAL_CAPACITY = 16;public Stack() {elements = new Object[DEFAULT_INITIAL_CAPACITY];}public void push(Object e) {ensureCapacity();elements[size++] = e;}public Object pop() {if (size == 0) {throw new EmptyStackException();}return elements[--size];}private void ensureCapacity() {if (elements.length == size) {elements = Arrays.copyOf(elements, 2 * size + 1);}}}

这段程序有一个内存泄露。如果一个栈先增长,再弹出元素,那么从栈中弹出的对象将不会被当做垃圾回收,即使栈的程序不再引用这些对象,它们也不会被 回收。
这是因为栈内部维护着这些对象的过期引用(obsolete reference)——指永远不会再被解除的引用。在本例中,凡是在elements数组的活动部分之外的任何引用都是过期的。活动部分指数组下标小于size的那些元素。也就是大于size那部分元素时过期引用,除非再次执行压栈操作。

这类问题的修复方法很简单:一旦对象引用已经过期,只需清空这些引用即可。对于上述例子,只要一个元素被弹出来,指向它的引用就过期了。pop()方法的修改版如下:

  public Object pop() {if (size == 0) {throw new EmptyStackException();}//size指向的是存在元素的后一个位置,--size可以同时将大小减1和指向栈顶元素Object result = elements[--size];elements[size] = null;return result;}

内存泄露的另一个常见的来源是缓存。对于这个问题,可以这样:只要在缓存之外存在对某个元素的键的引用,该元素就有意义,那么就可以用WeakHashMap代表缓存;当缓存中的元素过期之后,它们就会自动被删除。**只有当所要的缓存元素的生命周期是由该键的外部引用而不是由值决定时,WeakHashMap才有用处。

内存泄露的第三个常见来源是监听器和其他回调。如果你实现了一个API,客户端在这个API中注册回调,却没有显示地取消注册,那么除非你采取某些动作,否则它们就会不断地堆积起来。确保回调立即被当做垃圾回收的最佳方法是只保存它们的弱引用。

避免使用终结方法和清除方法

终结方法通常是不可预测的,也是很危险的,一般情况下是不必要的。Java9中的清除方法虽然没有那么危险,但仍然是不可预测、运行缓慢,一般情况下也是不必要的。

这两个方法的缺点在于不能保证被及时执行。注重实践的任务不应该由终结方法或清除方法来完成。

永远不应该依赖终结方法或清除方法来更新重要的持久状态。

使用终结方法的另一个问题是:如果忽略在终结过程中被跑出来的未被捕获的异常,该对象的终结过程也会终止。

那么终结方法和清除方法有什么好处呢?它们有两种合法用途。当资源的所有者忘记调用它的close()方法是,终结方法可以充当安全网。第二种用途与对象的本地对等体有关。本地对等体是一个本地对象(非Java对象),普通对象通过本地方法委托给一个本地对象。如果本地对等体没有关键资源,并且性能也可以接受的话,那么清楚方法正是执行这项任务最合适的工具。

try-with-resoures优于try-finally

以前,try-finally语句时确保资源会被适时关闭的最佳方法,就算发生异常或者返回也一样:

static String firstLineOfFile(String path) throws IOException {BufferedReader br = new BufferedReader(new FileReader(path));try {return br.readLine();} finally {br.close();}
}

但是如果有多个资源,或br.close中也抛出受检异常,那么代码就会很"ugly"

幸好,Java7引入了try-with-resource。但是使用它的资源要实现AutoCloseable接口。

static String firstLineOfFile(String path) throws IOException {try(BufferedReader br = new BufferedReader(new FileReader(path))) {return br.readLine();}}

当有多个资源时:

static void copy(String src,String dst) throws IOException {try(InputStream in = new FileInputStream(src));OutputStream out = new FileOutputStream(dst)) {byte[] buf = new byte[BUFFER_SIZE];int n;while((n = in.read(buf)) >= 0) {out.write(buf,0,n);}}
}

还可以使用catch子句

static String firstLineOfFile(String path,String defaultVal) {try(BufferedReader br = new BufferedReader(new FileReader(path))) {return br.readLine();}catch(IOException e) {return defaultVal;}
}

在处理必须关闭的资源时,始终要考虑优先用try-with-resoures而不是try-finally。这样得到的代码将更加简洁、清晰,产生的异常也更有价值。

《Effective Java 3rd》读书笔记——创建和销毁对象相关推荐

  1. Effective java 总结1 - 创建和销毁对象

    Effective java 总结 - 创建和销毁对象 第1条 用静态工厂方法代替构造器 优势 静态工厂方法有名称 不必每次调用的时候创建一个新的对象 可以返回原返回类型的任何子类型对象 返回对象的类 ...

  2. [Effective Java]第二章 创建和销毁对象

    第一章      前言 略... 第二章      创建和销毁对象 1.            考虑用静态工厂方法代替构造器 创建对象方法:一是最常用的公有构造器,二是静态工厂方法.下面是一个Bool ...

  3. 《Effective Java》读书笔记--创建和销毁对象

    2019独角兽企业重金招聘Python工程师标准>>> 考虑用静态工厂方法代替构造函数. 当我们在写一个工具类时,是不希望用户将该类实例化的,所以应该定义一个private的构造函数 ...

  4. Effective Java读书笔记(二)

    Effective Java 读书笔记 (二) 创建和销毁对象 遇到多个构造器参数时要考虑使用构建器 创建和销毁对象 何时以及如何创建对象? 何时以及如何避免创建对象? 如何确保它们能够适时地销毁? ...

  5. Effective Java 读书笔记(七):通用程序设计

    Effective Java 读书笔记七通用程序设计 将局部变量的作用域最小化 for-each 循环优于传统的 for 循环 了解和使用类库 如果需要精确的答案请避免使用 float 和 doubl ...

  6. Effective Java读书笔记---二、创建和销毁对象

    二.创建和销毁对象 何时以及如何创建对象, 何时以及如何避免创建对象, 如何确保它们能够适时地销毁, 如何管理对象销毁之前必须进行的各种清理动作 1.用静态工厂方法代替构造器 优势: 它们有名称 不必 ...

  7. Effective Java读书笔记

    序列化 谨慎的实现Serializable接口 实现Serializable最大的代价,一旦这个类被发布就大大降低了改变这个类实现的灵活性,这个类中所有私有实例域都将变成导出API的一部分,不符合最低 ...

  8. Effective Java读书笔记三:创建和销毁对象

    第1条:考虑用静态工厂方法代替构造器 对于类而言,为了让客服端获得它的一个实例最常用的的一个方法就是提供一个公有的构造器.还有一种方法,类可以提供一个公有的静态工厂方法(static factory ...

  9. Effective Java 读书笔记(一)

    前言: 开个新的坑位,<effective java>的读书笔记,之后有时间会陆陆续续的更新,读这本书真的感触满多,item01和item02就已经在公司的项目代码中看到过了.今天这篇主要 ...

  10. Effective Java读书笔记七:泛型(部分章节需要重读)

    第23条:请不要在新代码中使用原生态类型 从java1.5发行版本开始,Java就提供了一种安全的替代方法,称作无限制的通配符类型,如果要使用范型,但是确定或者不关心实际的参数类型,就可以用一个问号代 ...

最新文章

  1. ansible自动化运维(三)——Playbook实战
  2. NHibernate之Mapping 之 Property
  3. C语言memmove()函数: 复制内存内容(可以重叠的内存块)
  4. ios6.x越狱将不会再呈现了
  5. Jquery中css()方法获取边框长度
  6. node 没有界面的浏览器_node.js爬虫入门(二)爬取动态页面(puppeteer)
  7. opencv透视变换:GetPerspectiveTransform、warpPerspective函数的使用
  8. MySQL修改数据类型语句
  9. 百度正式发布PaddlePaddle深度强化学习框架PARL
  10. 数据平台-第一章-数据质量提升
  11. 阿里的下一个15年:大数据是核心
  12. linux桌面lxde 安装_八大理由支持选LXDE作为Linux桌面
  13. MySQL性能优化(三)Buffer Pool实现原理
  14. 计算机一级必考知识点,计算机一级考试基础知识点汇总.doc
  15. 听歌识曲算法技术[语音识别]
  16. WORD文档无法编辑解决
  17. cdc有哪些rapper_获谢帝推荐,合作Higher Brothers,CDC的rapper里竟还藏着这样一位狠角色...
  18. 冬令营2015 酱油记
  19. clickhouse lag/lead
  20. SCOUT 薄膜分析软件

热门文章

  1. 【本地存储】将数据存储到本地 (sessionStorage、vuex)
  2. http/tcp/ip/端口
  3. jQuery之美,第一次...
  4. Android--从相册中选取照片并返回结果
  5. an existing tansporter instance is currently uploading this package 解决方法
  6. 一个简单的登陆功能模块
  7. Django tips: 查看当前Request所执行的所有SQL
  8. (Origin)如何复制文件到另一个项目
  9. linux 的常用命令---------第十二阶段(smb、FTP服务)
  10. 网页布局02 盒子模型