预热:泛型

本文大量参考Thinking in java(解析,填充)。

定义:多态算是一种泛化机制,解决了一部分可以应用于多种类型代码的束缚。虽然我们可以在参数定义一个基类或者是一个接口,但是他们的约束还是太强了,有的时候我们更希望编写更通用的代码,使代码能够应用于“某种不具体的类型”.而正是泛型的出现解决了这类问题,它实现了参数化类型的概念。其最初的目的是希望类或方法能够具备最广泛的表达能力(通过解耦类或方法与所使用的 类型之间的约束)。在你创建参数化类型的一个实例时,编译器会为你负责转型操作,并且保证类的正确性。

泛型参数不能使用基本类型.

泛型类:

public class Holder<T> {
    private T a;
    public T getA() {
        return a;
    }
    
    public void setA(T a) {
        this.a = a;
    }
    
public static void main(String[] args) {
    Holder<Object> aHolder=new Holder<>();
    aHolder.setA("asd");
    aHolder.setA(new Object());
    
}
}```

创建Holder对象时可以指定泛型指向的对象,指明后就只能在Holder内部放入该类型(或其子类,多态与泛型不冲突),所以这里放入Object以及String都是可以的,Object是所有类的爹。

###泛型接口:同上

###泛型方法:

![泛型方法](http://upload-images.jianshu.io/upload_images/3267534-beeaebe61351a122.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
 方法可以独立于类而变化。无论何时,只要你能够做到,你就应该尽量使用泛型方法。在使用泛型类时我们创建对象需要去指定类型参数的值,而使用泛型方法的时候通常不需要明确指明,如上图,编译器会自动找出相应的值(type argument inference)。同样也可以显示指明类型:
            `response.<String>f("123");`
    当然泛型方法与可变参数之间也是可以共存的:

public class Holder1 {

public static <T>ArrayList<T> test(T...a)
{
ArrayList<T> result=new ArrayList<>();
for(T item:a)
{
result.add(item);
}
return result;
}

public static void main(String[] args) {
System.out.println(test("123","234","345"));

}
}

output:[123,234,345]

###擦除:
  尽管可以声明Arraylist.class但是无法声明ArrayList<Integer>.class.
    Class class1=new ArrayList<String>().getClass();
Class class2=new ArrayList<Integer>().getClass();
System.out.println(class1==class2);
output:true

通过这种方式可以看的更清楚:

public class Holder1 {

class A{};
class B{};

public static void main(String[] args) {
List<A> list =new ArrayList<A>();
HashMap<A, B> hashMap=new HashMap<>();
System.out.println(Arrays.toString(list.getClass().getTypeParameters()));
System.out.println(Arrays.toString(hashMap.getClass().getTypeParameters()));
}
}

output:
[E]
[K, V]
Class.getTypeParameters()将返回一个TypeVariavle对象数组,表示有泛型声明所声明的类型参数。然而输出的只是标识符,所以可以得出以下结论:
 在泛型代码内部是无法获得任何有关泛型参数类型的信息。
       
尽管你知道类型参数标识符和泛型类型边界这类的信息--你却无法知道用来创建某个特定实例的实际类型参数。
        这就是擦除带来的弊端。比如说:

public class Holder1<T> {
private T obj;
public Holder1(T obj) {
// TODO Auto-generated constructor stub
this.obj=obj;
}
public void test()
{
//obj.test(); wrong
}
public T getObj()
{
return obj;
}

public static void main(String[] args) {
Holder1<Test>holder1=new Holder1<Test>(new Test());
holder1.test();
}
}
class Test{
public void test()
{
System.out.println("sb");
}
}

由于擦除,这种调用在java中是无法实现的,而为了实现这种需求(obj需要调用f())就有了extends关键字,也就是泛型的边界
extends:  
` public class Holder1<T extends Test>`
<T extends Test>声明T必须具有类型Test或者从Test导出的类型.也就是当前定义的泛型必须是Test或其子类.
   
 为什么通过这样就能成功实现我们的需求?因为泛型类型参数在运行时是擦除到它的第一个边界,编译器实际上会把类型参数替换为它的最后擦除类,所以当前的T在擦除后实际上是Test,等于做了一个替换。
  
  那么这么做的意义在哪里,和我这样去写的差别在哪:

public class Holder1 {
private Test obj;
public Holder1(Test obj) {
// TODO Auto-generated constructor stub
this.obj=obj;
}
public Test getObj() {
return obj;
}
public void test()
{
obj.test();
}
}

根本原因是通过泛化,能让当前代码跨越多个类工作,它不明确定义某个字类型,在使用时能返回确切的类型信息。比如:

这里返回的就是我定义的Test1

public class Holder1<T extends Test> {
private T obj;
public Holder1(T obj) {
// TODO Auto-generated constructor stub
this.obj=obj;
}
public T getObj() {
return obj;
}
public void test()
{
obj.test();
}

public static void main(String[] args) {
Holder1<Test1>holder1=new Holder1<Test1>(new Test1());
holder1.getObj();//这里返回的就是我定义的Test1对象
}
}
class Test{
public void test()
{
System.out.println("sb");
}
}
class Test1 extends Test
{
}

为什么会有擦除,而不能像C++一样实现完整的泛化机制:
>    这就是为了保证向前的兼容性,java早期并没有泛型的相关概念,并且能够减少JVM相关的改变,以及不破坏现有类库的前提下,以最小代价来实现相关概念。
      而这也使得泛型在java当中不是那么好用。所以在运行时期,所有泛型都将被擦除,替换成它们的非泛型上界,例如List<T>这种将被擦除为List,而普通的类型变量在没定义边界的情况下被擦除为Object.

####擦除的代价:
不能用于显式地引用运行时类型的操作之中:转型(cast)、instanceof操作和new表达式。因为所有的参数类型信息在运行时期都会丢失,所以需要无时无刻提醒自己:参数的类型信息只是目前看起来拥有而已。最后只会留下它的上界。

###边界处的动作: 

public class Holder1<T> {
private Class<T> clazz;
public Holder1(Class<T> class1) {
// TODO Auto-generated constructor stub
clazz=class1;
}

public T[] Test() {
  return (T[]) Array.newInstance(clazz, 2);//这里强转,并且有cast警告
}

public static void main(String[] args) {
System.out.println(Arrays.toString(new Holder1<String>(String.class).Test()));

}
}

output:[null, null]

  这里即使clazz被存储为Class<T>,但是由于擦除,实际上也只是Class,因此在运行时Array.newInstance内的clazz没有实际含义。接上文描述的:在泛型代码内部是无法获得任何有关泛型参数类型的信息。所以这里Array.newInstance实际上并未拥有clazz蕴含的类型信息(这里的T没有实际意义,不知道实际上是什么)。
     而另一个例子;     

public class Holder1<T> {
private List<T> list;
public Holder1() {
}
public List<T> maker(T t,int n)
{
List<T>result=new ArrayList<>();
for(int i=0;i<n;i++)
{
result.add(t);
}
return result;
}

public static void main(String[] args) {
Holder1<String> sHolder1=new Holder1<>();
System.out.println(sHolder1.maker("asd", 6));
}
}

这个例子中,尽管在运行时会擦除所有T类型的相关信息,可是它仍旧可以确保在编译器你放置到Holder1当中的对象具有T类型,使其适合List<T>,确保了在方法或类中的类型内部一致性,这也可以认为是一种规法。不过还是那句话,内部并不知道T的实际含义.只能确保类型的统一.
    
因为擦除在方法体中移除了类型信息,所以在运行时的问题就是边界:对象进入和离开方法的地点。

 看下面两个例子:
1.不使用泛型

public class Holder1 {
private Object object;

public Holder1() {  
}
public void setObject(Object object) {
    this.object = object;
}
public Object getObject() {
    return object;
}   

public static void main(String[] args) {
Holder1 holder1=new Holder1();
holder1.setObject("String");
String string=(String) holder1.getObject();
}
}

 反编译后:  

D:>javap -c Holder1
Compiled from "Holder1.java"
public class Holder1 {
public Holder1();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":
()V
4: return
public void setObject(java.lang.Object);
Code:
0: aload_0
1: aload_1
2: putfield #2 // Field object:Ljava/lang/Object;
5: return
public java.lang.Object getObject();
Code:
0: aload_0
1: getfield #2 // Field object:Ljava/lang/Object;
4: areturn
public static void main(java.lang.String[]);
Code:
0: new #3 // class Holder1
3: dup
4: invokespecial #4 // Method "<init>":()V
7: astore_1
8: aload_1
9: ldc #5 // String String
11: invokevirtual #6 // Method setObject:(Ljava/lang/Obje
ct;)V
14: aload_1
15: invokevirtual #7 // Method getObject:()Ljava/lang/Obj
ect;
18: checkcast #8 // class java/lang/String
21: astore_2
22: return
}

 前面的一大部分省略,set和get都是针对Object操作,观察第18行得知,get之后会有一个checkcast类型检查,翻阅
Java Virtual Machine Online Instruction Reference:得知
>  checkcast:ensure type of an object or array, checks that the top item on the operand stack (a reference to an object or array) can be cast to a given type.

 翻译:确保一个Object或者Array的类型。检查操作数栈的最上层item(object或array的引用)是否能被转换成相应的类型。

 checkcast 实际上可以被认为是:

if (! (obj == null || obj instanceof <class>)) {
throw new ClassCastException();
}
// if this point is reached, then object is either null, or an instance of
// <class> or one of its superclasses.


2.使用泛型:

public class Holder1<T> {
private T object;

public Holder1() {  
}
public void setObject(T object) {
    this.object = object;
}
public T getObject() {
    return object;
}   

public static void main(String[] args) {
Holder1<String> holder1=new Holder1<>();
holder1.setObject("String");
String string=holder1.getObject();
}
}

反编译后:

public static void main(java.lang.String[]);
Code:
0: new #3 // class Holder1
3: dup
4: invokespecial #4 // Method "<init>":()V
7: astore_1
8: aload_1
9: ldc #5 // String String
11: invokevirtual #6 // Method setObject:(Ljava/lang/Obje
ct;)V
14: aload_1
15: invokevirtual #7 // Method getObject:()Ljava/lang/Obj
ect;
18: checkcast #8 // class java/lang/String
21: astore_2
22: return
}

11行setObject开始传入的就是Object,但是set()方法不需要类型检查,编译器已经检查过了,但是对get方法在18行checkcast还是进行了类型检查,只不过用了泛型以后由编译器自动插入,其实效果是一样的。
    
在泛型中所有动作发生在边界处---对传进来的值做额外的编译期检查,并由编译器插入传出去的值的转型。这都是在编译期间完成的。
    
###泛型数组:

class Gener<T>
{
public void gg()
{
System.out.println("ASD");
}
}
public class Holder1 {
static Gener<Integer>[] gia;

public Holder1() {  
    
}

public static void main(String[] args) {
// gia=(Gener<Integer>[]) new Object[10]; //编译器不会报错,但是运行会报错ClassCastException
// gia[0].gg();
gia=(Gener<Integer>[]) new Gener[10];
gia[0]=new Gener<Integer>();
gia[0].gg();
}

}

那么既然数组无论它们持有的类型如何,都具有相同的结构,看起来是可以创建一个Object数组并将其转型为所希望的数组类型。事实上这样做会报错,为什么呢。


 通过编写下述代码,进行反编译 

public class TT {
public static void main(String[] args) {
int[] aa=new int[10];
String[]bb=new String[10];
}
}

获得

public class TT {
public TT();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":
()V
4: return
public static void main(java.lang.String[]);
Code:
0: bipush 10 //push the int 10 onto the stack,将10放入堆栈中
2: newarray int
4: astore_1
5: bipush 10
7: anewarray #2 // class java/lang/String
10: astore_2
11: return
}

这里有个newarray和anewarray就是创建数组,同样翻阅
Java Virtual Machine Online Instruction Reference:
>newarray<type>:allocate new array for numbers or booleans.  
newarray is used to allocate single-dimension arrays of booleans, chars, floats, doubles, bytes, shorts, ints or longs.
    newarray pops a positive int, n, off the stack, and constructs an array for holding n elements of the type given by <type>. Initially the elements in the array are set to zero. A reference to the new array object is left on the stack.
  
   翻译:newarray<type>给numbers或者booleans分配一个新的数组。它被用来分配booleans, chars, floats, doubles, bytes, shorts, ints or longs. 的一维数组。
        newarray从堆栈中弹出一个正整数n,然后构造一个type是你定义的类型的数组。初始化数组当中所有的元素(都设置默认为0)。在堆栈上留下对新数组对象的引用.  
>anewarray<type>:allocate new array for objects.<type> is either the name of a class or interface, e.g. java/lang/String, or, to create the first dimension of a multidimensional array, <type> can be an array type descriptor, e.g. [Ljava/lang/String;  
        
        anewarray allocates a new array for holding object references. It pops an int, size, off the stack and constructs a new array capable of holding size object references of the type indicated by <type>.
       
        <type> indicates what types of object references are to be stored in the array (see aastore). It is the name of a class or an interface, or an array type descriptor. If it is java/lang/Object, for example, then any type of object reference can be stored in the array. <type> is resolved at runtime to a Java class, interface or array. See Chapter 7 for a discussion of how classes are resolved.
     
      A reference to the new array is pushed onto the stack. Entries in the new array are initially set to null.    

 翻译:给对象分配新的数组.<type>可以是class或者interface的名称,比如说java/lang/String,   或者为了创建多维数组的第一维,<type>可以是数组类型描述符,例如   [Ljava/lang/String;                              anewarray  分配一个新的数组去持有对象的引用。它从对战中弹出一个int类型的size(大小),并构造一个能够持有size个type对象引用的新数组。
     
  <type>表示object引用在数组当中是以什么类型被存储的。它是class或者interface的名称或者数组类型的描述。比如说type是 java/lang/Object,那么任意object引用都能被存储进当前数组中,而在运行时将<type>解析成java类,interface或者数组。
  
 一个新的数组引用将push到堆栈上,数组中所有条目都会被设置为null 
 
 从这里就可以看出,这里泛型数组的构建会调用anewarray,而anewarray需要明确的type,
那么这样就可以知道:
 1.在gia=(Gener<Integer>[]) new Object[10];   中,即使gia看起来是转型为Gener<Integer>[],但是这也只是在编译期,运行时他仍然是Object[],正是因为anewarray运行时已经将type定义为Object,你无法对底层的数组进行更改。所以强制转型会引起ClassCastException.

   2.根据Oracle的java文档来看,泛型属于Non_Reifiable type,而引起这部分的原因也是因为类型擦除Non_Reifiable type会在编译期被移除泛型信息,所以在运行时无法获取具体的类型信息。而java明确规定数组内的元素必须是reifiable的,所以类似T[] a=new T[10]这类型的无法通过编译。

参考例子:  

String[] strArray = new String[20];
Object[] objArray = strArray;
objArray[0] = new Integer(1); // throws ArrayStoreException at runtime

那么假如说泛型的数组可以直接创建:
ArrayList<String>[] a=new ArrayList<String>[];
那么随后也可以改为Object数组然后往里面放ArrayList<Integer>,我们在随后的代码中可以把它转型为Object[]然后往里面放Arraylist<Integer>实例。
这样做不但编译器不能发现类型错误,就连运行时的数组存储检查对它也无能为力,它能看到的是我们往里面放Arraylist的对象,我们定义的<String>在这个时候已经被抹掉了.

//下面的代码使用了泛型的数组,是无法通过编译的
GenTest<String> genArr[] = new GenTest<String>[2];
Object[] test = genArr;
GenTest<StringBuffer> strBuf = new GenTest<StringBuffer>();
strBuf.setValue(new StringBuffer());
test[0] = strBuf;
GenTest<String> ref = genArr[0]; //上面两行相当于使用数组移花接木,让Java编译器把GenTest<StringBuffer>当作了GenTest<String>
String value = ref.getValue();// 这里是重点!

最后一行中,根据之前讲到的泛型边界问题,取值的时候会是这样
(String)ref.getValue();所以会有ClassCastException.这个程序虽然看起来是程序员的错误,
而且也没有什么灾难性后果。但是从另一个角度看,泛型就是为了消灭ClassCastException出现的
而这个时候他自己却引发这个错误,这就矛盾了。通常来说如果使用泛型,只要代码编译时没有警告,那么就不会出现错误ClassCaseException。

  究竟泛型数组应该怎么用,我们可以参考ArrayList的源码

transient Object[] elementData; // non-private to simplify nested class access

它内部使用的就是Object[]

get方法对item使用了强转,才能让我们获取到正确的对象。

// Positional Access Operations
@SuppressWarnings("unchecked")
E elementData(int index) {
return (E) elementData[index];
}
/**
* Returns the element at the specified position in this list.
*
* @param index index of the element to return
* @return the element at the specified position in this list
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E get(int index) {
rangeCheck(index);
return elementData(index);
}

add方法涉及到向上转型

public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}

事实证明,ArrayList大量使用了强转去实现。
总的来说数组是协变的而泛型不是,如果 Integer扩展了 Number(事实也是如此),那么不仅 Integer是 Number,
而且 Integer[]也是 Number[],在要求 Number[]的地方完全可以传递或者赋予 Integer[]。
这也是矛盾发生的根本原因.

###边界:
简单点就是 <T extends Base>这样在擦除的时候T就会转换成Base,而在泛型类或方法中可以直接视同Base
的方法

public class Response {
public <T extends ABC>void f(T t)
{
t.test();

}
}
class ABC{
public void test()
{
System.out.println("asdasd");
}
}

另一种用法就是

interface has{
void test();
}
public class Ges<T extends Object & has> {
T mT;
public T getmT() {
return mT;
}
public void setmT(T mT) {
this.mT = mT;
}

继承一个父类与多个接口的形式。和类继承的用法是一样的,不过class必须在第一个,接口跟在后面
同样,如果希望将某个类型限制为特定类型或特定类型的超类型,请使用以下表示法:
<T super LowerBoundType>

###通配符:
泛型没有内建的协变类型,有时候想要在两个类型之间建立某种类型的向上转型关系:这正是通配符允许的

public class Ges {
public static void main(String[] args) {
List<? extends fruit> list=new ArrayList<apple>();
// list.add(new fruit());
// list.add(new apple()); all wrong
fruit fruit=list.get(0);//可以获取到fruit
}
}
class fruit{

}
class apple extends fruit{

}

尽管list类型是List<? extends fruit>但是这并不实际意味着List将持有任何类型的fruit。实际上你不能往这个list当中安全的添加对象。尽管他可以合法指向一个list<apple>,
你无法往里面丢任何对象。编译器只知道list内部的任何对象至少具有fruit类型,但是他具体是什么就不知道了。

每当指定add方法的时候,![](http://upload-images.jianshu.io/upload_images/3267534-3002ac82f9d98063.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)显示的都是null,由于不知道,所以干脆不接受任何类型的fruit。同样的编译器将直接拒绝对参数列表中
涉及通配符的方法的调用。

当然List<? extends fruit> list=new ArrayList<apple>();这种写法赋予了泛型一种协变性,
像之前提到过的:List<fruit> list=new ArrayList<apple>()是无法通过的因为假如这么写,
那就意味着可以list.add(new Banana());这就破坏了list定义时的承诺,它是一个苹果列表。

那么针对这种情况(无法往内部添加)可以使用超类型通配符:<? super Class> ,有了超类型通配符,你就可以进行写入了:
![](http://upload-images.jianshu.io/upload_images/3267534-dd793ecf68699d44.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
 这相当于定义了一个下界,无论你传入什么,最起码是apple,或者是他的子类,这样的类型传入是安全的.
这种写法也可以称之为逆变,同样的get方法返回的是Object.
###关于协变和逆变:
    
    什么是协变(逆变):
>如果A和B是类型(T),f表示类型转换,≤表示子类型关系,(例如A≤B,表示A是B的子类)那么:
如果A≤B 则f(A) ≤ f(B) 那么 f是协变的 (假如说T和f(T)序关系一致,就是协变)
如果A≤B 则f(B) ≤ f(A) 那么 f 是逆变的(假如说T和f(T)序关系相反,就是逆变)
如果上面两种都不成立,那么f是无关的

例如:class List<T>{...},可以理解为输入一个类型参数T,通过类型转换(f)成为,List<T>(f(T))
所以当A=Object,B=String,那么f(A)=List<Object>,f(B)=List<String>,可是f(A)与f(B)不具备
任何关系,所以单纯的泛型不具备协变性。

所以ArrayList<? extends fruit>=ArrayList<fruit>,(? extends fruit)<=fruit
,f(? extends fruit)=f(fruit),通过这种写法具备了协变性。

同样ArrayList<? super fruit>=ArrayList<fruit>,(? super fruit)>=fruit,
f(? extends fruit)=f(fruit),这就是逆变性。

#### 为什么要有协变逆变?优势在哪?
 而根据里氏替换原则:
 >派生类(子类)对象能够替换其基类(超类)对象被使用。当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。

从例子来看:

public class f {
// 定义三个类: Benz -> Car -> Vehicle,它们之间是顺次继承关系

// 测试函数
void test() {
    List<Vehicle> vehicles = new ArrayList<>();
    List<Benz> benzs = new ArrayList<>();
    Utils<Car> carUtils = new Utils<>();
    carUtils.put(vehicles, new Car());
    Car car = carUtils.get(benzs, 0);
    carUtils.copy(vehicles, benzs);
}

}
class Vehicle {}
class Car extends Vehicle {}
class Benz extends Car {}
// 定义一个util类,其中用到泛型里的协变和逆变
class Utils<T> {
T get(List<? extends T> list, int i) {
return list.get(i);
}

void put(List<? super T> list, T item) {
    list.add(item);
}

void copy(List<? super T> to, List<? extends T> from) {
    for(T item : from) {
        to.add(item);
    }
}

}

Car car = carUtils.get(benzs, 0);可以看出 List<Benz>对List<? extends Car>进行了替换(协变),

List<Benz>的get方法返回Benz对象,而List<? extends Car>返回的是Car对象,这符合替换原则,方法的后置条件(即方法的返回值)要比父类更严格。

carUtils.put(vehicles, new Car());List<Vehicle>对List<? super Car>进行替换(逆变),

List<Vehicle>的put方法需要Vehicle对象作为形参,而List<? super Car>需要的是Car,这就满足替换原则
的前置条件需求.

最后一个copy体现的是协变与逆变的汇总,替换。所以总的来说泛型的协变与逆变定义上界与下界,同时也让程序
能够在某种程度上满足替换原则,通过良好的替换让程序更具拓展性。这也是为什么大量的框架中
例如rxjava,会在参数中大量使用这种形式的泛型写法。

参考:
 Thinking in java
 http://ybin.cc/programming/java-variance-in-generics/
 https://www.zybuluo.com/zhanjindong/note/34147
 http://www.cnblogs.com/en-heng/p/5041124.html
 http://colobu.com/2015/05/19/Variance-lower-bounds-upper-bounds-in-Scala/#Java中的协变和逆变
 http://blog.csdn.net/hopeztm/article/details/8822545
 https://zh.wikipedia.org/wiki/%E9%87%8C%E6%B0%8F%E6%9B%BF%E6%8D%A2%E5%8E%9F%E5%88%99
 https://www.ibm.com/developerworks/cn/java/j-jtp01255.html
 https://www.zhihu.com/question/20928981
 http://cs.au.dk/~mis/dOvs/jvmspec/ref-Java.html
 http://www.blogjava.net/deepnighttwo/articles/298426.html
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 148,637评论 1 318
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 63,443评论 1 266
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 99,164评论 0 218
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,075评论 0 188
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,080评论 1 266
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,365评论 1 184
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 30,901评论 2 283
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,649评论 0 176
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,122评论 0 223
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,734评论 2 225
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,093评论 1 236
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,548评论 2 222
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,028评论 3 216
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,765评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,291评论 0 178
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,162评论 2 239
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,293评论 2 242

推荐阅读更多精彩内容

  • 第8章 泛型 通常情况的类和函数,我们只需要使用具体的类型即可:要么是基本类型,要么是自定义的类。但是在集合类的场...
    禅与计算机程序设计艺术阅读 2,121评论 6 10
  • 引言 泛型是Java中一个非常重要的知识点,在Java集合类框架中泛型被广泛应用。本文我们将从零开始来看一下Jav...
    横冲直撞666阅读 406评论 0 0
  • 2.简单泛型 -********Java泛型的核心概念:告诉编译器想使用什么类型, 然后编译器帮你处理一切细节 2...
    CodingHou阅读 377评论 0 0
  • 开发人员在使用泛型的时候,很容易根据自己的直觉而犯一些错误。比如一个方法如果接收List作为形式参数,那么如果尝试...
    时待吾阅读 983评论 0 3
  • 登高 杜甫 风急天高猿啸哀,渚[1]清沙白鸟飞回。 无边落木[2]萧萧[3]下,不尽长江滚滚来。 万里悲秋常作客,...
    古诗新读阅读 420评论 0 2