您的位置:首页 > 编程语言 > Java开发

11.Java 基础 - 泛型

2016-07-14 16:03 387 查看

基本概念

泛型的本质是参数化类型(Parameterized Type)的应用,也就是说所操作的数据类型被指定为一个参数,在用到的时候在指定具体的类型。

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

1.发展

在 JDK 1.5 之前,只能通过 Object 是所有类型的父类和类型强制转换两个特点的配合来实现类型泛化。

因此在编译期间,编译器无法检查这个 Object 的强制转型是否成功,这样容导致发生 ClassCastException (强制转换异常) 。

下面我们来看一个实例,就能知道泛型的作用:

不使用泛型的情况(1.5 之前)

ArrayList arrayList = new ArrayList();
arrayList.add(100);
arrayList.add("abc");

//因为不知道取出来的值的类型,类型转换的时候容易出错
String str = (String) arrayList.get(0);


使用泛型的情况(1.5 之后)

ArrayList<String> arrayList = new ArrayList<String>();
arrayList.add("abc");

//因为限定了类型,所以不能添加整形,编译器会提示出错
arrayList.add(100);


2.术语

// 以 ArrayList<E>,ArrayList<Integer> 为例:

ArrayList<E>:泛型类型

E:类型变量(或者类型参数)

ArrayList<Integer> :参数化的类型

Integer:类型参数的实例(或实际类型参数)

ArrayList :原始类型


3.探究

泛型类

class Demo<T> {
private T value;

Demo(T value) {
this.value = value;
}

public T getValue() {
return value;
}

public void setValue(T value) {
this.value = value;
}
}

public class Test {
public static void main(String[] args) {
Demo<String> demo = new Demo("abc");
demo.setValue("cba");
System.out.println(demo.getValue()); // cba
}
}


泛型接口

interface Demo<K, V> {
void print(K k, V v);
}

class DemoImpl implements Demo<String, Integer> {

@Override
public void print(String k, Integer v) {
System.out.println(k + "-" + v);
}
}

public class Test {
public static void main(String[] args) {
Demo<String, Integer> demo = new DemoImpl();
demo.print("abc", 100);
}
}


泛型方法

public class Test {
public static void main(String[] args) {
int num = get("abc", 100);
System.out.println(num);
}

// 关键 --> 多了 <K, V> ,可以理解为声明此方法为泛型方法
public static <K, V> V get(K k, V v) {

if (k != null) {
return v;
}
return null;
}
}


类型限定

类型限定在泛型类、泛型接口和泛型方法中都可以使用,不过要注意下面几点:

不管该限定是类还是接口,统一都使用关键字 extends

可以使用 & 符号给出多个限定

如果限定既有接口也有类,那么类必须只有一个,并且放在首位置。例如:

public static <T extends Comparable&Serializable> T get(T t1,T t2)


下面再来分析下类型限定的作用…

1.不对类型参数设置界限

观察下面的代码,在没有对类型参数进行类型限定时会出现编译错误。原因如下:

因为在编译之前,编译器并不能确认泛型类型(T)是什么类型

因此它默认 T 为原始类型(Object)。

所以只能调用 Object 的方法,而不能调用 compareTo 方法。

public static <T> T get(T t1,T t2) {
//编译错误
if(t1.compareTo(t2)>=0);
return t1;
}


2.对类型参数设置界限

当对类型参数 T 设置界限(bound)后,编译错误不再发生。因为此时编译器默认 T 的原始类型为 Comparable。

public static <T extends Comparable> T get(T t1,T t2) {
if(t1.compareTo(t2)>=0);
return t1;
}


类型擦除

Java 中的泛型基本上都是在编译器这个层次来实现的。

在生成的 Java 字节码中是不包含泛型中的类型信息的。

使用泛型时加上的类型参数,会在编译器在编译的时候去掉,这个过程就称为类型擦除。

来看下面的这个例子:

public class Test {
public static void main(String[] args) {
ArrayList<String> arrayList1 =new ArrayList<String>();
ArrayList<Integer> arrayList2 = new ArrayList<Integer>();
// true
System.out.println(arrayList1.getClass() == arrayList2.getClass());
}
}


观察代码,这里定义了两个ArrayList数组:

一个是ArrayList泛型类型,只能存储字符串,一个是ArrayList泛型类型,只能存储整形。

通过比较它们的类对象,发现结果为 true。

说明泛型类型 String 和 Integer 在编译过程中都被擦除掉了,只剩下了原始类型(即 Object)。

再来看一个例子:

public class Test {
public static void main(String[] args) throws Exception{
ArrayList<String> arrayList =new ArrayList<String>();
arrayList.add("abc");
arrayList.getClass().getMethod("add", Object.class).invoke(arrayList, 100);
for (int i=0;i<arrayList.size();i++) {
System.out.println(arrayList.get(i));
}
}
}


观察代码,这里定义了一个ArrayList 泛型类型实例化为 Integer 的对象

如果直接调用 add 方法,那么只能存储整形的数据。

利用反射调用 add 方法,却可以存储字符串。

说明 Integer 泛型实例在编译之后被擦除了,只保留了原始类型。

1.原始类型

原始类型(raw type)就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型

任意一个泛型的类型参数,都存在对应的原始变量。

一旦类型变量被擦除(crased),就会使用其限定类型(无限定的变量用 Object)替换。

// 此时 T 是一个无限定类型,所以原始类型就是 Object
class Pair<T> { }

// 类型变量有限定,原始类型就用第一个边界的类型变量来替换,即Comparable
class Pair<T extends Comparable& Serializable> { }

// 此时原始类型为 Serializable,编译器在必要的时要向 Comparable 插入强制类型转换
// 为了提高效率,应该将标签(tagging)接口(即没有方法的接口)放在边界限定列表的末尾
class Pair<T extends Serializable&Comparable>


2.类型参数的类型

在下面的例子中,类型参数指 T,T 的类型就是所谓的【类型参数】的类型。

观察代码,可以得出如下结论:

不指定【类型参数 T 】的类型,当参数的类型不一致时,原始类型取同一父类的最小级

指定【类型参数 T 】的类型时,原始类型只能为其指定的类型或类型的子类

public class Test {

// 定义泛型方法
public static <T> T add(T x, T y) {
return y;
}

public static void main(String[] args) {

// 1.不指定泛型

// 两个参数都是 Integer,所以 T 为 Integer 类型
int i = Test.add(1, 2);

// 两个参数分别是 Integer,Float,取同一父类的最小级,T 为 Number 类型
Number f = Test.add(1, 1.2);

// T 为 Object
Object o = Test.add(1, "asd");

// 2.指定泛型

// 指定了Integer,所以只能为 Integer 类型或者其子类
int a = Test.<Integer> add(1, 2);

//编译错误,指定了 Integer,不能为Float
int b=Test.<Integer>add(1, 2.2);

// 指定为Number,所以可以为 Integer,Float
Number c = Test.<Number> add(1, 2.2);
}

}


3.类型检查

泛型的类型检查是针对引用的,而不针对被引用的对象本身。

在下面的例子中,list 是引用对象,因此类型检查是针对它的。

// 没有进行类型检查,等价于 ArrayList list = new ArrayLis()
ArrayList list = new ArrayList<String>();
list.add(100);
list.add("hello");

// 进行编译检查,等价于 ArrayList<String> list = new ArrayList<String>();
ArrayList<String> list = new ArrayList();
list.add("hello");
list.add(100);  // 编译错误


4.类型擦除与多态的冲突

来看下面的例子,这里定义了一个泛型类 Parent,一个实现它的子类 Son,并在子类中重写了父类的方法。

class Parent<T> {
private T value;

public T getValue() {
return value;
}

public void setValue(T value) {
this.value = value;
}
}

class Son extends Parent<String>{
@Override
public void setValue(String value) {
super.setValue(value);
}
@Override
public String getValue(){
return super.getValue();}
}


在上面提到过泛型的类型参数在编译时会被类型擦除,因此编译后的 Parent 类如下:

class Parent {
private Object value;

public Object getValue() {
return value;
}

public void setValue(Object value) {
this.value = value;
}
}


此时对比 Parent 与 Son 的 getValue/setValue 方法,发现方法的参数类型已经改变,从 Object -> String,这也意味着不是重写(overrride) 而是重载(overload)。

然而调用 Son 的 setValue 方法, 发现添加 Object 对象时编译错误。说明也不是重载。

public class Test {
public static void main(String[] args) {
Son son = new Son();
son.setValue("hello");
// 关键 -->编译错误
son.setValue(new Object());
}
}


那么问题来了,通过上面的分析?Son 中定义的方法到底是重写还是重载?答案是:重写。这里 JVM 采用了桥方法(Brige)来解决类型擦除和多态引起的冲突。

我们对 Son 进行反编译(”Javap -c 类名.class”),得到如下内容:

Compiled from "Test.java"
class Son extends Parent<java.lang.String> {
Son();
Code:
0: aload_0
1: invokespecial #8                  // Method Parent."<init>":()V
4: return

public void setValue(java.lang.String);
Code:
0: aload_0
1: aload_1
2: invokespecial #16                 // Method Parent.setValue:(Ljava/lang/Object;)V
5: return

public java.lang.String getValue();
Code:
0: aload_0
1: invokespecial #23                 // Method Parent.getValue:()Ljava/lang/Object;
4: checkcast     #26                 // class java/lang/String
7: areturn

public java.lang.Object getValue();
Code:
0: aload_0
1: invokevirtual #28                 // Method getValue:()Ljava/lang/String;
4: areturn

public void setValue(java.lang.Object);
Code:
0: aload_0
1: aload_1
2: checkcast     #26                 // class java/lang/String
5: invokevirtual #30                 // Method setValue:(Ljava/lang/String;)V
8: return
}


发现这里共有 4 个 setValue/getValue 方法,除了 Son 表面上重写的 String 类型,编译器又自己生成了 Object 类型的方法,也称为桥方法。结果就是,编译器通过桥方法真正实现了重写,只是在访问时又去调用表面的定义的方法。

注意事项

不能用基本类型实例化类型参数,可以用对应的包装类来实例化类型参数

// 编译错误
ArrayList<int> list = new ArrayList<int>();

// 正确写法
ArrayList<Integer> list = new ArrayList<Integer>();


参数化类型的数组不合法

Demo<T >{
}

public class Test {
public static void main(String[] args) {
// 编译错误 --> 类型擦除导致数组变成 Object [],因此没有意义
Demo<String>[ ]  demo =new Demo[10];
}
}


不能实例化类型变量

// 编译错误,需要类型参数需要确定类型
Demo<T> demo = new Demo<T>


泛型类的静态上下文中不能使用类型变量

public class Demo<T> {
public static T name;
public static T getName() {
...
}
}


不能抛出也不能捕获泛型类的对象

//异常都是在运行时捕获和抛出的,而在编译的时候,泛型信息全都会被擦除掉。会导致这里捕获的类型一致

try{
}catch(Problem<Integer> e1){
//do Something...
}catch(Problem<Number> e2){
// do Something ...
}


参考

http://blog.csdn.net/lonelyroamer/article/details/7868820
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  Java 泛型