泛型(1) - 泛型类 , 泛型接口

什么是泛型?为什么需要泛型?

  • 泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参列表,普通方法的形参列表中,每个形参的数据类型是确定的,而变量是一个参数。在调用普通方法时需要传入对应形参数据类型的变量(实参),若传入的实参与形参定义的数据类型不匹配,则会报错。

参数化类型是什么?以方法的定义为例,在方法定义时,将方法签名中的形参的数据类型也设置为参数(也可称之为类型参数),在调用该方法时再从外部传入一个具体的数据类型和变量。

泛型使用场景

ArrayList 集合中,可以放入所有类型的对象,假设现在需要一个只存储了 String 类型对象的 ArrayList 集合。

@ Test
public void test() {
ArrayList list = new ArrayList();
list.add("aaa");
list.add("bbb");
list.add("ccc");
for (int i = 0; i < list.size(); i++) {
System.out.println((String)list.get(i));
}
}
  • 上面代码没有任何问题,在遍历 ArrayList 集合时,只需将 Object 对象进行向下转型成 String 类型即可得到 String 类型对象。

但如果在添加 String 对象时,不小心添加了一个 Integer 对象,会发生什么?看下面代码:

@ Test
public void test() {
ArrayList list = new ArrayList();
list.add("aaa");
list.add("bbb");
list.add("ccc");
list.add(111);
for (int i = 0; i < list.size(); i++) {
System.out.println((String)list.get(i));
}
} // 报错 ....

上述代码在编译时没有报错,但在运行时却抛出了一个 ClassCastException 异常,其原因是 Integer 对象不能强转为 String 类型。

  • 那如何可以避免上述异常的出现?即我们希望当我们向集合中添加了不符合类型要求的对象时,编译器能直接给我们报错,而不是在程序运行后才产生异常。这个时候便可以使用泛型了。
@ Test
public void test() {
ArrayList<String> list = new ArrayList<>();
list.add("aaa");
list.add("bbb");
list.add("ccc");
list.add(111);// 在编译阶段(敲代码时就会出现红线),程序无需跑起来,编译器会报错
for (int i = 0; i < list.size(); i++) {
System.out.println((String)list.get(i));
}
}

< String > 是一个泛型,其限制了 ArrayList 集合中存放对象的数据类型只能是 String,当添加一个非 String 对象时,编译器会直接报错。这样,我们便解决了上面产生的 ClassCastException 异常的问题(这样体现了泛型的类型安全检测机制)。

泛型概述小结

  1. 与使用 Object 对象代替一切引用数据类型对象这样简单粗暴方式相比,泛型使得数据类型的类别可以像参数一样由外部传递进来。它提供了一种扩展能力,更符合面向对象开发的软件编程宗旨。

  2. 当具体的数据类型确定后,泛型又提供了一种类型安全检测机制,只有数据类型相匹配的变量才能正常的赋值,否则编译器就不通过。所以说,泛型一定程度上提高了软件的安全性,防止出现低级的失误。

  3. 泛型提高了程序代码的可读性。在定义泛型阶段(类、接口、方法)或者对象实例化阶段,由于 < 类型参数 > 需要在代码中显式地编写,所以程序员能够快速猜测出代码所要操作的数据类型,提高了代码可读性。

泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法,下面将正式介绍泛型的相关知识。

泛型类

  • 类型参数用于类的定义中,则该类被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种容器类,如:ListSetMap等。

基本语法如下:

class 类名称 <泛型标识> {
private 泛型标识 /*(成员变量类型)*/ 变量名;
.....

}
}
  • 尖括号 <> 中的 泛型标识被称作是类型参数,用于指代任何数据类型。
  • 泛型标识是任意设置的(如果你想可以设置为 Hello都行),Java 常见的泛型标识以及其代表含义如下:
T :代表一般的任何类。
E :代表 Element 元素的意思,或者 Exception 异常的意思。
K :代表 Key 的意思。
V :代表 Value 的意思,通常与 K 一起配合使用。
S :代表 Subtype 的意思,文章后面部分会讲解示意。

举例如下:

public class Generic<T> { 
// key 这个成员变量的数据类型为 T, T 的类型由外部传入
private T key;

// 泛型构造方法形参 key 的类型也为 T,T 的类型由外部传入
public Generic(T key) {
this.key = key;
}

// 泛型方法 getKey 的返回值类型为 T,T 的类型由外部指定
public T getKey(){
return key;
}
}
  • 一般来讲,在泛型类中,静态方法和静态变量不可以使用泛型类所声明的类型参数
public class Test<T> {    
public static T one; // 编译错误
public static T show(T one){ // 编译错误
return null;
}
}

/*
由此可见, 在泛型类中只有三处位置可以定义参数
1. 非静态的成员属性类型
2. 非静态方法的形参类型(包括非静态成员方法和构造器)
3. 非静态的成员方法的返回值类型
*/

// 泛型类还可以接受多个参数
public class MultiType <E,T> {
E value1;
T value2;

public E getValue1(){
return value1;
}

public T getValue2(){
return value2;
}
}

泛型类的使用

  • 在创建泛型类的对象时,必须指定类型参数 T 的具体数据类型,即尖括号 <> 中传入的什么数据类型,T 便会被替换成对应的类型。如果 <> 中什么都不传入,则默认是 < Object >。
// 这里我定义一个 Generic 泛型类
public class Generic<T> {

private T key;

public Generic(T key) {
this.key = key;
}

public T getKey(){
return key;
}
}

// 使用 该泛型类
@ Test
public void test() {
Generic<String> generic = new Generic<>();// 传入 String 类型

// <> 中什么都不传入,等价于 Generic<Object> generic = new Generic<>();
Generic generic = new Generic();
}
  • 在传入String后的原泛型类的拓展:
public class Generic { 

private String key;

public Generic(String key) {
this.key = key;
}

public String getKey() {
return key;
}
}
  • 可以发现,泛型类中的类型参数 T 被 <> 中的 String 类型全部替换了。
  • 使用泛型的上述特性便可以在集合中限制添加对象的数据类型,若集合中添加的对象与指定的泛型数据类型不一致,则编译器会直接报错,这也是泛型的类型安全检测机制的实现原理。

泛型接口

泛型接口和泛型类的定义差不多,基本语法如下:

public interface 接口名<类型参数> {
...
}

// 举例如下:
public interface Inter<T> {
public abstract void show(T t) ;
}

重要!泛型接口中的类型参数,在该接口被继承或者被实现时确定。解释如下:

  • 1. 定义一个泛型接口
interface IUsb<U, R> {

int n = 10;
U name;// 报错! 接口中的属性默认是静态的,因此不能使用类型参数声明

R get(U u);// 普通方法中,可以使用类型参数

void hi(R r);// 抽象方法中,可以使用类型参数

// 在jdk8 中,可以在接口中使用默认方法, 默认方法可以使用泛型接口的类型参数
default R method(U u) {
return null;
}
}

// 注意:在泛型接口中,静态成员也不能使用泛型接口定义的类型参数。

2. 定义一个接口 IA 继承了 泛型接口 IUsb,在 接口 IA 定义时必须确定泛型接口 IUsb 中的类型参数。

// 在继承泛型接口时,必须确定泛型接口的类型参数
interface IA extends IUsb<String, Double> {
...
}

// 当去实现 IA 接口时,因为 IA 在继承 IUsu 接口时,指定了类型参数 U 为 String,R 为 Double
// 所以在实现 IUsb 接口的方法时,使用 String 替换 U,用 Double 替换 R
class AA implements IA {
@Override
public Double get(String s) {
return null;
}
@Override
public void hi(Double d) {
...
}
}

3. 定义一个类 BB 实现了 泛型接口 IUsb,在 类 BB 定义时需要确定泛型接口 IUsb 中的类型参数。

// 实现接口时,需要指定泛型接口的类型参数
// 给 U 指定 Integer, 给 R 指定了 Float
// 所以,当我们实现 IUsb 方法时,会使用 Integer 替换 U, 使用 Float 替换 R
class BB implements IUsb<Integer, Float> {
@Override
public Float get(Integer integer) {
return null;
}
@Override
public void hi(Float afloat) {
...
}
}

4. 定义一个类 CC 实现了 泛型接口 IUsb 时,若是没有确定泛型接口 IUsb 中的类型参数,则默认为 Object。

// 实现泛型接口时没有确定类型参数,则默认为 Object
// 建议直接写成 IUsb<Object, Object>
class CC implements IUsb {//等价 class CC implements IUsb<Object, Object>
@Override
public Object get(Object o) {
return null;
}
@Override
public void hi(Object o) {
...
}
}

5. 定义一个类 DD 实现了 泛型接口 IUsb 时,若是没有确定泛型接口 IUsb 中的类型参数,也可以将 DD 类也定义为泛型类,其声明的类型参数必须要和接口 IUsb 中的类型参数相同。

// DD 类定义为 泛型类,则不需要确定 接口的类型参数
// 但 DD 类定义的类型参数要和接口中类型参数的一致
class DD<U, R> implements IUsb<U, R> {
...
}