泛型总结与补充

当把对象放进java集合中的时候,集合就会忘记这个对象的数据类型,从而保证了通用性,但是也产生了一些问题。
对于java集合,在编译的时候是不检查类型的,因此取出集合的时候是需要进行强制类型转换的,可能会引发ClassCastException异常。JDK5之后,发展起来的。

所谓泛型就是在定义类、接口、方法的时候使用类型形参,这个类型形参将在声明变量、创建对象、调用方法的时候使用类型形参,这个类型形参将在声明变量、创建对象、调用方法的时候动态的指定(即传入实际的类型参数、可以称为类型实参)。

更新

1
2
3
ArrayList<String> s = new ArrayList<String>();
//JDK7之前是不支持的
ArrayList<String> s = new ArrayList<>();
  • 泛型的实质是:允许在定义接口、类时声明类型形参,类型形参在整个接口、类体内可当成类型使用。几乎所有可使用普通类型的地方都可以使用这种类型形参。
  • 类名中添加了泛型之后,在写类的构造器的时候,不需要添加
  • 泛型具有继承性
  • 不管泛型的实际类型参数是什么,他们在运行的时候总有同样的类.在内存中也只占用一块内存,因此在静态方法,静态代码块,静态变量的声明过程中,都不允许使用类型形参。
1
2
3
ArrayList<String> s = new ArrayList<>();
ArrayList<Integer> s2 = new ArrayList<>();
System.out.println(s.getClass()==s2.getClass()); //true
  • 由于系统中并不会真正的生成泛型类,所以instanceof 运算符后不能使用泛型类

    使用类型通配符

    为了表示各种类型List的父类,我们使用类型统配符,类型统配符是一个问号(?),将一个问号作为类型实参传给List集合,写作:List<?> (意思是未知类型元素的List)。这个问号(?)被称为统配符,它的元素类型可以匹配任何类型。

下面这个程序中使用的时候需要注意,

1
2
3
ArrayList<?> s = new ArrayList<String>();
s.add(new Object());
// 这样写会出错的

因为,add方法是有类型参数E作为集合的元素类型,所以传递给add的参数必须是E类的对象或者子类,但是添加null是可以的,因为null是所有引用类型的实例。
如果调用get方法是可以的,其返回值是一个未知类型,但是可以肯定的是它总是一个Object。

类型通配符的上限

如果我们不想List<?>是所有类型的父亲类,只想表示它是某一类泛型List的父类。可以使用下面例子

1
2
// shape是 一个抽象类 ,那么list中就可以使用继承自shape的类
List<? extends Shape>

因为我们不知道这个受限制通配符的具体类型,因此,我们也不能将Shape类的子类添加到List中。

设定类型形参的上限

1
2
// Number 是List 类型形参的上限
List<T extends Number>

泛型方法

我们在定义方法的时候,可以使用类型形参,所谓的泛型方法就是在声明方法的时候定义一个或多个类型形参。使用格式如下:
修饰符 <T,S> 返回值类型 方法名(形参列表 ){
//方法体
}

多个形参类型之间以逗号隔开,所有的类型形参声明放在方法修饰和方法返回值类型之间

例如:

1
2
3
4
5
static <T> void fromArrayTocollection(T[] a,Collection<T> c){
for(T O : a){
c.add(o)
}
}

使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Generical {
static <T> void addArray(T[] a,Collection <T> c) {
for(T o:a) {
c.add(o);
}
}
public static void main(String [] args) {
Object [] oa = new Object[10];
Collection <Object> coll = new ArrayList<>();
addArray(oa,coll);
String [] ss = new String[3];
Collection<String> coll2 = new ArrayList<>();
addArray(ss,coll2);
}
}

方法声明的形参只能在该方法里面使用,而接口、类声明中定义的类型形参则可以在整个接口以及类中使用。

泛型方法和类型统配符的区别

大多数时候都可以使用泛型方法来代替类型统配符。例如:

1
2
3
4
5
boolean containAll(Collection<?> c);
boolean addAll(Collection<? extends E> c)
可以 替换成
<T> boolean containAll(Collection<T> c);
<T extends E> boolean addAll(Collection<T> c)

上面两个方法中类型形参T只使用了一次,类型形参T使用的产生的唯一效果是可以在不同的调用点传入不同的实际类型。类型统配符就是被设计用来支持灵活的子类化的。
泛型方法允许类型形参被用来表示方法的一个或多个参数之间的类型依赖关系,或者方法返回值与参数之间的类型依赖关系,如果没有依赖关系就不要使用泛型。

泛型构造器

Java 允许在构造器签名中声明类型形参。

1
2
3
4
5
6
7
8
9
10
11
public class Foo {
public <T> Foo(T t){
System.out.println(t);
}
}

//测试
public static void main(String [] args) {
new Foo("CEHSI");
new Foo(20);
}

Java7新增加的“菱形”

前面介绍的Java7新增“菱形”语法,它允许调用构造器时在构造器后使用一对尖括号来代表泛型信息。但如果程序显示指定了泛型构造器中声明的类型形参的实际类型,则不可以使用“菱形”语法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Foo<E> {
public <T> Foo(T t){
System.out.println(t);
}
}


public class Generical {
public static void main(String [] args) {
Foo<String> mc1 = new Foo<>(5);
Foo<String> mc2 = new <Integer>Foo<String>(5);
//编译会错误 因为即指定了泛型构造器中的类型形参是Integter类型,又想使用“菱形”语法
Foo<String> mc3 = new <Integer>Foo<>(5);
}
}

设定统配符下限

为了表达A元素与B元素相同或者是B元素的父类,这种约束关系,java允许设定统配符的下限<? super Type> 这个统配符表示它必须是Type本身或者是Type 的父类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static <T> T copy(Collection<? super T> dest,Collection<T> src) {
T last = null;
for(T ele : src) {
last = ele;
dest.add(ele);
}
return last;
}
public static void main(String [] args) {
List<Number> ln = new ArrayList<>();
List<Integer> li = new ArrayList<>();
li.add(5);
Integer d = copy(ln,li);
System.out.println(d);
}

泛型方法与方法重载

因为泛型既可以允许设定统配符的上限,也允许设定统配符的下限,但是在Jdk-1.8 之后就不支持。这里的jdk1.8是我现在使用的版本。在jdk1.8之前具体到哪一个版本,没有测试。

1
2
3
4
5
public static <T> void copy(Collection<T> dest,Collection<? extends T> src) {
}
public static <T> T copy(Collection<? super T> dest,Collection<T> src) {
return null;
}

擦除和转换

在严格的泛型代码里面,带泛型声明的类总应该带着类型参数。但是为了与老的JAVA代码保持一致,也允许在使用带泛型声明的类时不指定实际的类型参数。如果没有为这个泛型类指定实际的类型参数,则该类型参数被称作是原始类型,默认是声明该参数的第一个上限类型。

当把一个具有泛型信息的对象赋值给另一个没有泛型信息的变量的时候,所有在尖括号之间的类型信息都将会被扔掉。
例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Apple <T extends Number>{
T size;
public Apple() {}
public Apple(T size) {
this.size = size;
}
public void setSize(T size) {
this.size = size;
}
public T getSize() {
return this.size;
}
}
1
2
3
4
5
6
7
8
Apple<Integer> a = new Apple<>(6);
//a 的getsize方法返回Integer
Integer as = a.getSize();
//把a 的对象赋值给Apple变量,丢失尖括号里面的里类型信息
Apple b = a;
//b 只能知道size的类型是Number
Number size1 = b.getSize();
//Integer size2 = b.getSize();

可以看到上面的一个示例,我们首先创建了一个Apple的类,是带有泛型声明为Integer的,其类型的上限是Number,这个类型形参用来定义Apple类的size变量,然后又创建了一个Apple的类,该类是没有泛型的,当我们把带有泛型信息的a对象的泛型信息赋值给不带有泛型信息的b对象的时候,即所有尖括号里面的信息都会丢失—–因为Apple 的类型形参的上限是Number类,编译器依然知道b的getsize()方法返回的是Number类型,但是具体是哪一个就不清楚了。

当我们将一个有着泛型类型的类a,赋值给没有泛型类型的类b的时候,就会出现丢失类的变量类型信息的现象,称为擦除。

1
2
3
4
5
6
7
8
9
Apple<Integer> a = new Apple<>(6);
//a 的getsize方法返回Integer
Integer as = a.getSize();
//把a 的对象赋值给Apple变量,丢失尖括号里面的里类型信息
Apple b = a;
//b 只能知道size的类型是Number
Number size1 = b.getSize();
Apple<Double> c = b;
Double size2 = (Double)c.getSize();

我们在将没有泛型类型的类b,赋值给一个带有泛型类型的类c,这时候是可以编译通过的,只是会出现“未经检查的转换”警告。实际上引用的还是Apple的这个类型,当我们尝试通过强制转换把它转换成一个Double的类型的时候,就会出现运行时异常java.lang.ClassCastException

泛型与数组

java5的泛型有一个很重要的设计原则,如果一段代码在编译的时候没有提出“[unchecked]”未经检验的异常的时候,那么程序在运行的时候是不会引发ClassCastException的异常的。

1
2
//这种操作是错误的原因是 数组不能包含类型变量或者类型形参 
List<String> [] test = new List<String>[10];

如果我们把上面的形式改为下面的格式,则为:

1
2
//这个在java 5 中在编译的时候会出现 [unchecked] 异常
List<String> [] test = new List[10];

在java 7 中是允许出现这个情况的。

1
2
3
4
5
List<String> [] test = new ArrayList[10];
List<String> li = new ArrayList<String>();
li.add("2");
test[0] = li;
System.out.println(test[0].get(0));

Java 8 允许使用无上限的泛型通配符,但是在Java 5中,如果使用无上限的泛型通配符是需要进行 强制类型转换的。

1
2
3
4
5
6
7
8
9
10
11
//java允许使用无上限的泛型统配符
List<?> [] test = new ArrayList<?>[10];
List<String> li = new ArrayList<String>();
li.add("2");
List<Integer> li2 = new ArrayList<Integer>();
li2.add(3);
test[0] = li;
test[1] = li2;
System.out.println(test[0].get(0));
System.out.println(test[1].get(0));
}