JavaSE-Adventure (III): 泛型程序设计

CONTENTS

  • JavaSE-Adventure (III): 泛型程序设计
    • 概述
      • 泛型概念
      • 泛型的提出背景
      • 泛型的作用
    • 使用泛型
      • 泛型类
      • 泛型接口
      • 泛型方法
    • 参数化类型
      • Type Parameter / Type Arugument
      • Parameterized Type / Raw Type
      • 泛型与继承
        • 泛型类的继承规则
          • 泛型类型的继承规则
    • 类型擦除
    • ? 通配符
      • 边界 无界
      • 类型通配符的上限
      • 类型通配符的下限
    • 桥接方法

概述

泛型概念

泛型本质是参数化类型,也就是说所操作的数据类型被指定为一个参数(type parameter)。

这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。

参数化类型其实可以这样理解,假设我们有一个容器,容器需要存储什么类型的数据呢?可以通过定义:

class Holder <T> {T property;
}Holder<String> holder = new Holder<>();

这样就把所存储的数据,的类型,实现了参数化,在实例化容器的时候,用参数的形势限定容器内存储的数据类型是什么。

泛型的提出背景

泛型出现的主要原因是为了创建容器类,能够用来指定容器要持有什么类型的对象,由编译器来保证类型的正确性

想象一下,如果没有泛型(Generics)的情况下,容器可以通过持有Object 来保证容器可以持有任意类型的对象,来实现参数的“任意化”,“任意化”带来的缺点是要作显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以在预知的情况下进行的。对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常,这是一个安全隐患

泛型的作用

  1. 泛化
    可以用T代表任意类型。Java语言中引入泛型是一个较大的功能增强,不仅语言、类型系统和编译器有了较大的变化,以支持泛型,而且类库也进行了大翻修,所以许多重要的类,比如集合框架,都已经成为泛型化的了,这带来了很多好处。

  2. 类型安全
    泛型的一个主要目标就是提高Java程序的类型安全,使用泛型可以使编译器知道变量的类型限制,进而可以在更高程度上验证类型假设。如果不用泛型,则必须使用强制类型转换,而强制类型转换不安全,在运行期可能发生ClassCast Exception异常,如果使用泛型,则会在编译期就能发现该错误。(将错误提前到编译期)

  3. 消除强制类型转换
    泛型可以消除源代码中的许多强制类型转换,这样可以使代码更加可读,并减少出错的机会。

  4. 向后兼容
    支持泛型的Java编译器可以用来编译经过泛型扩充的Java程序(Generics Java程序),但是现有的没有使用泛型扩充的Java程序仍然可以用这些编译器来编译。

使用泛型

泛型类

public class Holder<T> {private T item;public void setItem(T item) {this.item = item;}public T getItem() {return item;}
}

泛型接口

public interface Generator<T> {T next();
}

始终不确定泛型的类型,直到创建对象时,确定泛型的类型

public class GenericGenerator<T> implements Generator<T> {@Overridepublic T next() {return null;}
}

定义类时确定泛型的类型

public class StringGenerator implements Generator<String> {@Overridepublic String next() {return null;}
}

泛型方法

泛型方法可以存在于非泛型类或是泛型类。

public <T> T print(T param){System.out.println(param.getClass());System.out.println(param);return param;
}
  1. public 与 返回值中间<T>可以理解为声明此方法为泛型方法。只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法,如:
// 不属于泛型方法
public T next() {return null;
}
  1. <T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T

  2. 方法定义中 返回值与参数类型都能定义为泛型 T

参数化类型

Type Parameter / Type Arugument

介绍另一个概念 Type Parameter 和 Type Argument, 翻译过来就是形参和实参

方法定义中的参数变量被称为形参,实际传递给方法的值称为实参。如下:

public void method(Integer int) { }
obj.method(10);

例子中的int就是形参,10 则是实参

应用到泛型中就是Type Parameter / Type Argument

class Box<T> {}
Box<Integer> box = new Box<>();

Box是一个泛型类,T为其形参。使用时我们指定Integer作为泛型类Box的实参。

常用形参:

E:表示 Element,即元素,运用在集合中
K:表示 Key,即键
V:表示 Value,即值
N:表示 Number,即数值类型
T:表示 Type,即 Java 类型
?:表示不确定的 Java 类型

Parameterized Type / Raw Type

首先理解一个概念:
虚拟机没有泛型类型对象,所有对象都是普通类。无论何时定义泛型,都自动提供了一个原始类型(Raw Type)。原始类型就是直接删去类名后的泛型类型。

示例:

public class A<T> { }
A<String> classA = new A<>();
  • Type Parameter:T,T 是类 A 的类型参数
  • Type Argument: String,这里<String> 是类A 的Type Argument,可以理解为调用方法时的实参。
  • Parameterized TypeA<String>
  • Raw Type:A, A classA = new A();

泛型与继承

IntegerNumber 的子类。
ArrayList<Integer>ArrayList<Number> 的子类吗?

答案是否定的,并且ArrayList<Integer>ArrayList<Number> 之间没有联系。

并且这样的规则对于类型安全来说是十分有必要的。

泛型类的继承规则

先看一个类 A的定义,定义了一个类型参数 T

public class A<T> {public T name;public T getName() {return name;}
}

现在想要定义一个普通的非泛型类的类B ,想要继承 类A

public class B extends A<T> { }

这段代码是会编译报错的,想象一下如果这段代码可以通过编译,
那么B类的定义就会变成

public class B extends A<T> {// 从A 继承的代码public T name;public T getName() {return name;}...
}

由于我们定义了一个普通类而非泛型类,假如我们需要实例化B 时,通过以下语句发现:B classB = new B(); 我们无法通过类型实参的方式初始化 B中继承到的域的类型,name 的类型仍然是 T。显然这样的代码是有问题的。虚拟机根本不认识泛型,它需要确定的类型。显然,此时B类就会产生错误,因为它的代码存在T这种虚拟机不认识的类型。

public class B extends A<String> {// 从A 继承的代码public String name;public String getName() {return name;}...
}

这时可以直接通过 B b = new B(); 来实例化一个类B

B类此时相当于继承了一个“普通类”,
父类A将会用String 作为类型实参赋值给A 中定义的泛型。
此时A中各个变量类型都是确定的。

这就是规则1:当子类不是泛型类时,泛型父类必须被赋予类型常量

再看下一条规则,还是一样的类 A

public class A<T> {public T name;public T getName() {return name;}
}

想要定义一个泛型类类B 想要继承类A。
那么父类可以是泛型变量,也可以是泛型常量
下面展示两段继承后的代码:

public class B<T> extends A<String> {// 从A 继承的代码public String name;public String getName() {return name;}...
}
public class B<T> extends A<T> {// 从A 继承的代码public T name;public T getName() {return name;}...
}

还有一种思考

public class B<T> extends A {// 从A 继承的代码public Object name;public Object getName() {return name;}...
}

这段代码的意思是,类B 是泛型类,继承自类A,且类A 为Raw Type,可以理解为类A 中的泛型信息都被擦除成了 Object。同样public class B extends A {} 也是一样的道理。

泛型类型的继承规则

泛型不会继承普通类的继承关系

Box<Number>
Box<Integer>

这两个类并没任何关系

类型擦除

根据上面的概念,在泛型代码内部,无法获得任何有关泛型参数类型的信息。

擦除类型变量(erasure),并替换为限定类型(无限定使用Object), 如:

public class Holder<T> {private T item;public void setItem(T item) {this.item = item;}public T getItem() {return item;}
}

擦除后:

public class Holder {private Object item;public void setItem(Object item) {this.item = item;}public Object getItem() {return item;}
}

编译时泛型类型会被擦除,泛型擦除后调用 Holder#getItem ,这个返回值类型实际上变成了Object,编译器会自动加上强制类型转换,也就是说编译器会在任何使用到泛型的地方(存/取) 加上强制类型转换

如果生成泛型类对象时指定了具体的Type Argument,就称之为Parameterized Types

List<String> result= new ArrayList<>(); // parameterized type
List<String> result= new ArrayList(); // raw type

? 通配符

public class Box<E> {private E item;// ...setter - getter
}

先明确一个概念: ? 是类型实参,而不是类型形参

再看一段代码:

public static void showBox(Box<Number> box) {System.out.println(box.getItem());
}
Box<Integer> box = new Box<>();
showBox(box); //  compile error

上面已经介绍过了 Box<Number>Box<Integer> 没有继承关系,因此在这里不能运用多态的思想。

public static void showBox(Box<Integer> box) {System.out.println(box.getItem());
}

并且不能将泛型类运用在重载上,showBox(Box<Integer> box)showBox(Box<Number> box) 在编译其就会报错,因为泛型都会被擦除,相当于定义了两个相同方法。

想要定义一个能存储任意类型的容器,可以使用 ? 通配符定义如下方法

public static void showBox(Box<?> box) {Object item = box.getItem();
}
Box<Integer> box1 = new Box();
Box<Number> box2 = new Box();
showBox(box1);
showBox(box2);

但是取出是的对象类型却是 Object的

边界 无界

先看一段代码:

public class TypeTest<T> {private T item;public void useParameterizedType() {item.toString(); // 只能调用可以用Object 调用的方法}
}

经过上面的类型擦除的介绍之后,我们知道item 在编译时会被擦除成 Object 类型,因此此时只能调用 Object 中的方法。

此时定义的泛型形参<T> 也是无界的。

类型通配符的上限

Java 泛型重用了 extends 关键字,能够将泛型参数限制为某个类型的子集。此时为泛型参数定义了上界。
语法:类<? extends 类型实参>

含义:要求该泛型的类型,只能是已定义的实参类型,或实参类型的子类类型

/*** Box<> 泛型最大能传Number 或是Number的子类* @param box*/
public static void showBox(Box<? extends Number> box) {Number item = box.getItem();System.out.println(item);
}Box<Number> box = new Box<>(100);
showBox(box);Box<Integer> box2 = new Box<>(200);
showBox(box2);

Box<? extends Number> 解析:

  • ? : 表示Box的泛型是任意类型但仅限于Number子类
  • 可以往容器中存 Number 或 其子类,取出是,只能是Number
  • extends 关键字把Box 的泛型限定在Number及其子类,往里存时,我们可以存Number 或者它的子类,但是取出时,因为泛型信息都被擦除了,虚拟机没法知道存进去的是Integer 或是Double ,因此只能被当作是Number 类型,这里也是符合多态的概念的。
public class Animal { }
public class Cat extends Animal { }
public class MiniCat extends Cat { }public static void main(String[] args) {ArrayList<Animal> animals = new ArrayList<>();ArrayList<Cat> cats = new ArrayList<>();ArrayList<MiniCat> miniCats = new ArrayList<>();showAnimal(animals); // compile errorshowAnimal(cats);showAnimal(miniCats);
}/*** 泛型通配符上限,传递的集合类型只能是Cat 或其子类,取出时只能取出Cat* 我们通过extends 关键字限定传入showAnimal 方法的集合类型上限是Cat,* 因此我们传入的集合类型可以是List<Cat> List<MiniCat>** 因为虚拟机并不会知道showAnimal 接收的是Cat 还是 MiniCat,* 取出时我们无法调用MiniCat 的方法,只能确定最上级的类的类型是Cat,* 因此只能调用上界类Cat 的相关方法** @param list*/
private static void showAnimal(ArrayList<? extends Cat> list) {for (Cat cat : list) {System.out.println(cat);}// list.add(new Animal());  // compile error// list.add(new Cat()); // compile error// list.add(new MiniCat()); // compile error
}

泛型通配符上限,传递的集合类型只能是Cat 或其子类,取出时只能却出Cat。

我们发现往集合中添加元素时会报错,这里可能会很困惑。

因为我们集合元素类型的上限是Cat, 并不知道往里存入的是什么类型的元素
举几个例子:

// ArrayList<Animal> 可以存储的数据类型
ArrayList<Animal> animals = new ArrayList<>();
animals.add(new Animal());
animals.add(new Cat());
animals.add(new MiniCat());
// ArrayList<Cat> 可以存储的数据类型
ArrayList<Cat> cats = new ArrayList<>();
cats.add(new Cat());
cats.add(new MiniCat());
// ArrayList<MiniCat> 可以存储的数据类型
ArrayList<MiniCat> miniCats = new ArrayList<>();
miniCats.add(MiniCat);
// showAnimal 可以传递的集合 cats,miniCats
showAnimal(ArrayList<? extends Cat> list)

showAnimal() 接收List<Cat>, List<MinICat> 集合,也就是说此时,在showAnimal() 方法内部只能知道接收到的list是一个存储了Cat 类型的 或者是MiniCat,extends 关键字限定了集合数据类型必须是Cat 或者其子类。并不知道具体的是什么类型的集合。

在我们已知 MiniCat 是最小的类型时,我们认为showAnimal() 方法内可以使用过list#add 可以添加 一个MiniCat 对象,但是日后新增了一个MiniCat的子类 TomCat,showAnimal() 接收的是一个 List<TomCat> 时,往这个集合添加一个MiniCat对象,这样的行为肯定是不允许的,因此,使用 通配符上限时,无法进行添加操作

另一个例子:

AbstactCollection {public boolean addAll(Collection<? extends E> c) {boolean modified = false;for (E e : c)if (add(e))modified = true;return modified;}
}
List<Cat> cats = new ArrayList<>();
List<MiniCat> miniCats = new ArrayList<>();
cats.addAll(miniCats);

addAll() 方法限定类传入的集合泛型的上限是 E,则必须是E 或其子类,因此 cats.addAll(cats), cats.addAll(miniCats) 都是合法的,因为父类的集合必然是可以存子类对象的。

类型通配符的下限

语法:类<? super 类型实参>

含义:要求该泛型的类型,只能是实参类型,或实参类型的父类类型

/*** 泛型通配符下限,传递的集合类型只能是Cat 或其父类,取出时只能取出Object* 我们通过 super 关键字限定传入showAnimal 方法的集合类型下限是Cat* 因此我们可以存入 List<Cat> List<Animal> List<Object>,* 但是虚拟机无法确定我们存入的到底是何种集合类型,如果传入的是Object 的集合* 那必然是无法调用Cat 的相关方法,因此取出是只能取出Object 类型来使用** @param list*/
private static void showAnimal(ArrayList<? super Cat> list) {list.add(new MiniCat());list.add(new Cat());// list.add(new Animal()); compile error// list.add(new Object()); compile errorfor (Object o : list) {System.out.println(o);}
}
ArrayList<Animal> animals = new ArrayList<>();
ArrayList<Cat> cats = new ArrayList<>();
ArrayList<MiniCat> miniCats = new ArrayList<>();showAnimal(animals);
showAnimal(cats);
// showAnimal(miniCats); compile error// list.add(new Animal()); compile error
// list.add(new Object()); compile error

当我们用 super 关键字限定 showAnimal() 方法的下限时,我们可以往集合添加元素,但是元素的类型限定于 类型实参的子类,取出时只能取出Object 类型的数据。

因为 showAnimal() 方法接收List<Cat>, List<Animal>, List<Object> 集合,也就是说此时,在showAnimal 方法内部只能知道接收到的list是一个存储了Cat 类型的 或者是Animal或是Object,并不知道实际存储的是什么,只知道存储的数据类型下限时 Cat,因此我们往集合中添加Cat,或是MiniCat 时是完全合法的,因为Cat 和 MiniCat 都必然是上面几种类型的子类,super 关键字限定了集合数据类型必须是 Cat 或其父类。

而取出操作时,我们只知道集合数据类型的下限是Cat,并不知道showAnimal 接收到参数是 List<Cat> 还是 List<Animal> 还是 List<Object> ,所以取出只能是取出Object 类型来使用。

public TreeSet(Comparator<? super E> comparator) {this(new TreeMap<>(comparator));
}

这里TreeSet 的构造参数接收一个Comparator 类型的比较器,这个比较器限定了类型参数的下限是 E 也就是TreeSet 的类型参数。

TreeSet<Cat> set = new TreeSet<>(new ComparatorAnimal());

假如我们初始化一个类型参数是Cat 的TreeSet, 那么表明我们set 中存储的都是Cat 或是其子类MiniCat。

并且我们初始化一个比较器,我们知道比较器中的compare 方法需要获取到集合中的元素的域进行比较,假如我们传入的比较器的类型参数是 MiniCat,而我们集合此时存的是Cat,比较器需要拿MiniCat 的域进行比较,而这个域我们Cat 是没有的,此时代码肯定是有问题的,因此TreeSet 构造器的参数把Comparator 的类型参数通过 super 关键字,把下限限定成 TreeSet 的泛型数据类型,因此比较器的类型参数必须是Cat 或是Cat 的父类,所以比较器不可能会用TreeSet 数据类型的子类取进行比较,比较器能获取到的域都是集合的类型参数限定的,或是其父类的域,这是合理的。

这就是super 关键字的应用场景之一。

桥接方法

Java中的桥接方法(Bridge Method)是一种为了实现某些Java语言特性而由编译器自动生成的方法。可以通过使用Java反射中 Method 类的 isBridge() 方法来判断该方法是否是桥接方法。通过反射 Class.getMethod("") 取出的不是桥接方法。

在字节码文件中,桥接方法会被标记为 ACC_BRIDGEACC_SYNTHETIC,其中 ACC_BRIDGE 表示该方法是由编译器产生的桥接方法, ACC_SYNTHETIC 表示该方法是由编译器自动生成。

协变返回类型是指子类方法的返回值类型不必严格等同于父类中被重写的方法的返回值类型,而可以是更具体的类型,即子类重写父类方法时,返回的类型可以是子类方法返回类型的子类

public class Parent {public Number get(){return 0;}
}class Child1 extends Parent {public Number get(){return 1;}
}class Child2 extends Parent {public Integer get(){return 2;}
}

Child2 中多了一个 public java.lang.Number get(); 方法, 这个方法就是桥接方法,可以通过该方法的 flags 看到它含有 ACC_BRIDGE 标志。

在Java语言中,认为只要方法名和参数列表一致就是同一个方法,而JVM则认为方法名、参数列表和返回类型全部一样才是同一方法。而桥接方法的签名和其父类的方法签名一致,以此就实现了协变返回值类型。

泛型是Java 1.5才引进的概念,在这之前是没有泛型的概念的,但泛型代码能够很好地和之前版本的代码很好地兼容,这是因为,在编译期间Java编译器会将类型参数替换为其上界(类型参数中extends子句的类型),如果上界没有定义,则默认为Object,这就叫做类型擦除。当一个子类在继承(或实现)一个父类(或接口)的泛型方法时,在子类中明确指定了泛型类型,那么在编译时编译器会自动生成桥接方法。

public class Parent<T> {public Number get(T key){return 0;}
}class Child1 extends Parent<String> {public Number get(String key){return 1;}
}class Child2 extends Parent<String> {public Integer get(String key){return 2;}
}

对比一下 Child1 和 Child2 的字节码可以看出,Child2 中多了一个 public java.lang.Number get(java.lang.Object); 方法, 这个方法就是桥接方法,可以通过该方法的 flags 看到它含有 ACC_BRIDGE 标志。还可以看出这个桥接方法的参数类型是 Object 类型,它与父类的参数类型是一致的,包括返回值也是与父类的类型是一致的。它的方法内容就是先进行类型检查(第31行),然后再通过 invokevirtual 指令调用自身的 get(java.lang.String); 方法。

对于JVM来说,当它编译时,它会直接把类型进行擦除。

参考:
Java泛型解惑之 extends T>和 super T>上下界限:https://blog.csdn.net/ystyaoshengting/article/details/86674481
Java泛型语法,原理,高级用法和局限:https://www.jianshu.com/p/fd776fcb125f
百度百科 java泛型:https://baike.baidu.com/item/java%E6%B3%9B%E5%9E%8B/511821?fr=aladdin
Java底层知识:什么是 “桥接方法” ?https://www.jianshu.com/p/30c2c3de7871
JVM如何理解Java泛型类(转):https://www.cnblogs.com/ggjucheng/p/3352519.html
关于泛型继承的实验与猜想:https://zhuanlan.zhihu.com/p/31647953
泛型<? extends T>和<? super T>:https://blog.csdn.net/qq_44779215/article/details/110979876
Java 桥接方法:https://www.cnblogs.com/dwtfukgv/p/14887575.html

JavaSE-Adventure(III): Generics 泛型程序设计相关推荐

  1. Part10 泛型程序设计与C++标准模板库 10.1泛型程序设计及STL的结构

    1泛型程序设计的基本概念 泛型程序设计: 编写不依赖于具体数据类型的程序 将算法从特定的数据结构中抽象出来,成为通用的 C++的模板为泛型程序设计奠定了关键的基础 术语:概念 用来界定具备一定功能的数 ...

  2. Java基础语法十二 泛型程序设计

    1 意义 泛型程序设计意味着编写的代码可以被很多不同类型的对象所重用. 常见应用 : ArrayList 2 K T V E ? object等的含义 类型变量使用大写形式 E – Element ( ...

  3. java泛型程序设计——反射和泛型

    [0]README 0.1) 本文描述+源代码均 转自 core java volume 1, 旨在理解 java泛型程序设计 的 反射和泛型 的相关知识: [1]反射和泛型相关 1.1)现在, Cl ...

  4. java泛型程序设计——无限定通配符+通配符捕获

    [0]README 0.1) 本文描述+源代码均 转自 core java volume 1, 旨在理解 java泛型程序设计 的 无限定通配符+通配符捕获 的相关知识: [1]无限定通配符相关 1. ...

  5. java泛型程序设计——通配符类型+通配符的超类型限定

    [0]README 0.1) 本文描述+源代码均 转自 core java volume 1, 旨在理解 java泛型程序设计 的 通配符类型+通配符的超类型限定 的知识: [1]通配符类型相关 1. ...

  6. java泛型程序设计——泛型类型的继承原则

    [0]README 0.1) 本文描述+源代码均 转自 core java volume 1, 旨在理解 java泛型程序设计 的 泛型类型的继承原则 的知识: [1]泛型类型的继承原则相关 1.1) ...

  7. java泛型程序设计——注意擦除后的冲突

    [0]README 0.1) 本文描述+源代码均 转自 core java volume 1, 旨在理解 java泛型程序设计 的 注意擦除后的冲突 的知识: 1.1)当泛型类型被 擦除时, 无法创建 ...

  8. java泛型程序设计——泛型类的静态上下文中类型变量无效+不能抛出或捕获泛型类的实例

    [0]README 0.1) 本文描述+源代码均 转自 core java volume 1, 旨在理解 java泛型程序设计 的 泛型类的静态上下文中类型变量无效+不能抛出或捕获泛型类的实例 的知识 ...

  9. java泛型程序设计——Varargs 警告+不能实例化类型变量

    [0]README 0.1) 本文描述+源代码均 转自 core java volume 1, 旨在理解 java泛型程序设计 的 Varargs 警告+不能实例化类型变量 的知识: [1] Vara ...

最新文章

  1. ubuntu14.04如何在线安装eclipse以及C/C++开发组件,搭建软件开发平台
  2. spring-cloud-eureka服务注册与发现
  3. 微信小程序技巧-让特定组件首页始终展示修改编译条件即可,不用改json
  4. OpenCV辅助对象(help objects)(3)——Ptr
  5. 大数据是如何作用于实体经济
  6. oracle gi 创建,浅谈Oracle RAC --GI的启动
  7. 阶段1 语言基础+高级_1-3-Java语言高级_06-File类与IO流_09 序列化流_5_InvalidClassException异常_原理...
  8. N!阶层末尾有多少0
  9. 工作量统计系统 python_软件测试工作量统计新方法
  10. 如何用计算机算分组数据方差,『分组数据如何Excel计算标准差』Excel表格求分组数据的方差...
  11. c# 中通快递对接_中通快递-单号查询接口-物流路由跟踪信息快递鸟api对接教程...
  12. JQuery学习——标签页(Tabs)
  13. HTML——表白树动画
  14. 腾讯云HTTPDNS 将上线微信服务平台!
  15. 如何实现受管控的安全文件传输MFT?
  16. Landesk桌面管理之服务器管理篇
  17. 苹果iOS系统下的推送机制及实现
  18. springboot 将本地引用的lib一起打包
  19. 极客时间 算法训练营 毕业总结
  20. 项目新增commitLint 和 husky 步骤

热门文章

  1. 安装thunderbird_在Thunderbird中创建签名
  2. php45 上海北诺,Bio-Gel P6 生物胶P100 Bio-Rad 150-1940
  3. 【NLP】深度文本匹配综述
  4. 数据分析思维分析方法和业务知识——实战案例跨境电商行业
  5. oracle等待事件4——buffer busy wait 特别介绍
  6. (转)2017年12月宋华教授携IBM中国研究院、猪八戒网、中航信托、33复杂美共同论道智慧供应链金融...
  7. 面试题:1000瓶酒找1瓶毒酒
  8. 如何整理撰写舆情信息报告的方法技巧
  9. Dynamic Memory Based Attention Network for Sequential Recommendation【论文解读】
  10. crash工具使用方法