泛型概述-基本概念

2018-02-27 11:31:57来源:https://www.jianshu.com/p/3e19a83feee4作者:realxz人点击

分享




泛型程序设计 (Generic programming) 意味着编写的代码可以被很多不同的类型的对象所重用。



原始类型(Raw Type)

下面我们会用一些例子来说明,为什么使用泛型编写的代码可以被不同类型的对象所重用。在没有泛型之前 ArrayList 的代码是这样的:


public class RawArrayList {
private Object[] element;
public Object getElement(int position) {
//...
}

public void addElement(Object element) {
//...
}
}

public class RawArrayListTest {
public static void main(String[] args) {
RawArrayList rawArrayList = new RawArrayList();
rawArrayList.addElement("string");
rawArrayList.addElement(new File("file"));
String string = (String) rawArrayList.getElement(0);

//此处会出现类型转换异常
String file = (String) rawArrayList.getElement(1);
}
}

上面的代码有两个问题:


编译器没有错误检查,我们可以调用 setElement("string") 方法向 RawArrayList 中放入一个 String 类型的字符串,之后我仍然可以向其中放入一个其他类型的对象,例如 setElement(new File("file"))。编译器并不会有任何警告。
因为 RawArrayList 内部使用 Object 数组 来存储对象,这样我们在获取对象的时候就必须使用强制类型转换,String string = (String) rawArrayList.getElement(0);,由于 RawArrayList 没有对放入的类型做限制,所以就有可能出现类型转换异常 java.lang.ClassCastException。
类型参数


泛型提供了类型参数 (type parameters) 来帮助我们改善上述的代码。
可以认为是给 RawArrayList 中声明一个参数,这个参数就代表着列表中元素的类型,我们会在声明 RawArrayList 的时候指明参数的具体类型。



类型参数用尖括号加任意字母表示 : <T>,字母一般为单个大写字母,并有一定含义,例如 T(type),E(element),K(key),V(value) 等等


下面来看看用使用类型参数之后的 ArrayList:


public class ArrayListTest {
public static void main(String[] args) {
ArrayList<String> stringList = new ArrayList<String>();
stringList.add("string");
// 下面一行代码,编译器会报错,无法将 File 对象应用于 String 类型的 ArrayList
// stringList.add(new File("file"));
String string=stringList.get(0);
System.out.println(string);
}

// print > string
}

ArrayList<String> stringList = new ArrayList<String>(); 这一行代码中,可以省略创建 ArrayList 对象的时候传递的参数类型如 ArrayList<String> stringList = new ArrayList<>();,编译器可以从声明中推断出省略的类型。


注意尖括号不能省略,不然可能造成类型不安全的隐患( 这相当于将原始类型的对象传递给泛型类型的引用 )。例如我们将之前的内部具有 String 类型和 File 对象的 RawArrayList 传递给 ArrayList<String> 类型的引用,会造成什么影响呢?


 RawArrayList list=new RawArrayList();
list.add(1);
list.add("string");
ArrayList<String> stringArrayList=list;
for (String s : stringArrayList) {
System.out.println(s);//boom
}

很不幸会发生类型转换异常,我们最好不要将原生类型和泛型类型这样使用,除非你能保证类型安全。


泛型的一个目的就是尽早的发现可能出现的异常,在使用了泛型提供的类型参数之后,有两个显着的好处是


我们不需要自己进行类型转换了,编译器能推断出返回类型,可读性提高
编译器会对插入数据做类型检查,避免插入了错误的类型,安全性提高

好像 RawArrayList 代码的例子没有明显体现出我们在开头所说的,泛型代码可以被很多不同的类型的对象所重用。


接下来在让我们看看水果和果盘的例子:


Apple


public class Apple {
private String name;
public Apple(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

Orange


public class Orange {
private String name;
public Orange(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

现在我们再来定义一个果盘,用来存放水果。我们需要定义一个苹果果盘,用来存放苹果,再定义一个橘子果盘,用来存放橘子。


ApplePlate


public class ApplePlate {
private List<Apple> appleList;
public ApplePlate(List<Apple> appleList) {
this.appleList = appleList;
}
public void setAppleList(List<Apple> appleList) {
this.appleList = appleList;
}
public List<Apple> getAppleList() {
return appleList;
}

}

OrangePlate


public class OrangePlate {
private List<Orange> orangePlate;
public OrangePlate(List<Orange> orangePlate) {
this.orangePlate = orangePlate;
}
public List<Orange> getOrangePlate() {
return orangePlate;
}
public void setOrangePlate(List<Orange> orangePlate) {
this.orangePlate = orangePlate;
}
}

将苹果放进苹果果盘


private static void createApple() {
//生成苹果
List<Apple> apples = new ArrayList<>();
Apple apple1 = new Apple("苹果1");
Apple apple2 = new Apple("苹果2");
Apple apple3 = new Apple("苹果3");
apples.add(apple1);
apples.add(apple2);
apples.add(apple3);
//将苹果放入苹果果盘
ApplePlate applePlate = new ApplePlate(apples);
//取出刚放入的苹果们
for (Apple apple : applePlate.getAppleList()) {
System.out.println(apple.getName());
}
}

现在将橘子放进橘子果盘的话,只需要按照 createApple() 方法在编写一个 createOrange() 就可以了。


那如果现在我要新增一个水果类型怎么办,我还需要对应的再增加一个该水果类型的果盘。而且可以看到,我们水果的属性,方法,果盘的方法,除了类型不同之外,没什么不同。这时候就可以使用泛型来解决这个问题。


泛型类/接口

先让我们看看泛型类的概念:



具有一个或者多个类型参数的类/接口,就是泛型类/泛型接口



在定义类的时候,我们在类名的后面加上一个形如 <T> 的类型参数。类中属性的声明,方法的参数类型,包括返回类型等,都可以用类型 T 替代。泛型接口与泛型类的定义相同,我们就不展开叙述了。


现在我们将果盘(XXXPlate)改写为泛型类是什么样子


public class Plate<T> {
private List<T> fruitList;
public Plate(List<T> fruitList) {
this.fruitList = fruitList;
}
public List<T> getFruitList() {
return fruitList;
}
public void setFruitList(List<T> fruitList) {
this.fruitList = fruitList;
}
}

现在我们先抽象出一个水果类 Fruit


public class Fruit {
private String name;
public Fruit(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

为了对比,我们新创建一种水果类型 Cherry 使他继承 Fruit


public class Cherry extends Fruit {
public Cherry(String name) {
super(name);
}
}

首先通过抽象将共有的属性和方法抽象到父类中去,这样子类只需要实现一个构造函数即可。


接下来我们看看,使用了泛型之后,是如何将 Cherry 装进果盘中去的。


public static void createCherry() {
//生成车厘子
List<Cherry> cherryList = new ArrayList<>();
Cherry cherry1 = new Cherry("车厘子1");
Cherry cherry2 = new Cherry("车厘子2");
Cherry cherry3 = new Cherry("车厘子3");
cherryList.add(cherry1);
cherryList.add(cherry2);
cherryList.add(cherry3);
//将刚买的车厘子放入车厘子果盘
Plate<Cherry> cherryPlate = new Plate<>(cherryList);
for (Cherry cherry : cherryPlate.getFruitList()) {
System.out.println(cherry.getName());
}
}

假如我们现在又增加了一种水果 Pear ,这个时候我们只需要将 Plate<T> 中的类型参数指定为 Pear 这个样子 Plate<Pear> 即可。


这就体现了我们上面所说的,泛型代码可以被不同类型的对象所重用。可以这么认为:我们封装了一套数据结构和算法,用来处理一类操作,他与具体的类型无关,或者与限定的类型有关,这个时候,我们就可以使用泛型,只关注具体的操作,不用关心具体的类型。


泛型方法


我们类比泛型类可以知道,泛型方法就是具有一个或者多个类型参数的方法



将类型参数 <T> 放在修饰符的后面,返回类型的前面,这样我们的返回值,方法中的局部变量,参数类型都可以指定为我们声明的 T 类型。


我们这样定义一个泛型方法:


 public static <T> T getMiddleFruit(Plate<T> plate) {
int middle = plate.getFruitList().size();
return plate.getFruitList().get(middle);
}

这段代码的意思是,获取 Plate<T> 中间的元素,也就是获取果盘最中间的水果。


我们可以这样来调用它:


public static void createCherry() {
/...省略之前创建水果,将水果放进果盘的操作
Cherry middleCherry=PlateUtils.getMiddleFruit(cherryPlate);
System.out.println(middleCherry.getName());
}

泛型限定

在回头看我们上面定义的泛型类 Plate<T>,我们的类型参数是没有做任何限定的,类型参数 T 可以在声明的时候被指定为任何类型。


虽然我将 Plate,定义为果盘,可以传进来任何类型的水果,但其实由于我没有对 T 做任何的限定,那就意味着我们在声明的时候可以传递任意类型。


如下我们定义一个动物类型 Animal


public class Animal {
private String name;
public Animal(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

然后我们来尝试将一群动物放进果盘中:


public static void createAnimal() {
List<Animal> animalList = new ArrayList<>();
Animal dog = new Animal("Dog");
Animal cat = new Animal("Cat");
animalList.add(dog);
animalList.add(cat);
Plate<Animal> animalPlate = new Plate<>(animalList);
for (Animal animal : animalPlate.getFruitList()) {
System.out.println(animal.getName());
}
}

尴尬

最新文章

123

最新摄影

闪念基因

微信扫一扫

第七城市微信公众平台