Java基础 -- 深入理解泛型

一般的类和方法,只能使用具体的类型:要么是基本类型,要么是自定义的类。如果要编写可以应用于多种类型的代码,这种刻板的限制对代码的束缚就会很大。

而泛型很好的解决了这个问题,这也是Java SE5的重大变化之一,下面将会深入介绍泛型。

一 泛型的概念

泛型在java中有很重要的地位,在面向对象编程及各种设计模式中有非常广泛的应用。

1、什么是泛型?为什么要使用泛型?

泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。

泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

2、一个的例子

在前面博客中,我曾经介绍过Java持有对象(容器)。不过,有一个问题当时忽略了:在Java SE5之前的容器是允许向其中插入不正确类型。例如,考虑一个Apple对象的容器,使用最基本最可靠的容器ArrayList,在本例中Apple和Orange都被放置在了容器中,然后通过get()将他们取出,正常情况下,Java编译器会报告警告信息,因为这个示例没有使用泛型。在这里,使用Java SE5所特有的注解来抑制警告信息,注解以“@”符号开头,接受参数rawtypes。

import java.util.*;

class Apple{
    private static long counter;
    private final long id = counter++;
    public long id() {
        return id;
    }
}


class Orange{}

public class ApplesAndOrangesWithoutGenerics {
    @SuppressWarnings("rawtypes")
    public static void main(String[] args) {
        ArrayList apples = new ArrayList();
        for(int i=0;i<3;i++) {
            apples.add(new Apple());
        }
        
        apples.add(new Orange());
        
        for(int i=0;i<apples.size();i++) {
            System.out.println(((Apple)apples.get(i)).id());
        }
    }
}

输出结果:

0
1
2
Exception in thread "main" java.lang.ClassCastException: Orange cannot be cast to Apple
    at ApplesAndOrangesWithoutGenerics.main(ApplesAndOrangesWithoutGenerics.java:25)

Apple和Orange类是有区别的,他们除了都是Object之外没有任何共性。因为ArrayList保存的是Object,因此我们不仅可以通过ArrayList的add()方法将Apple对象放进这个容器,还可以添加Orange对象,而且在无论在编译期还是运行期都不会有问题。但是当使用ArrayList的get()方法取出我们认为是Apple的对象时,我们得到的只是Object引用,必须将其转换为Apple,因此需要将整个表达式括起来,在调用Apple的id()方法前,强制类型转换。否则,将会得到语法错误。在运行时,当尝试将orange对象转换为Apple时,就会抛出异常。为了解决这样的问题,泛型应运而生,通过使用泛型,就可以在编译期防止将错误类型的对象放置到容器中。

下面还是这个例子,但是使用了泛型:

import java.util.ArrayList;

public class ApplesAndOrangesWithGenerics {    
    public static void main(String[] args) {
        ArrayList<Apple> apples = new ArrayList<Apple>();
        for(int i=0;i<3;i++) {
            apples.add(new Apple());
        }
        
        //apples.add(new Orange());
        
        for(int i=0;i<apples.size();i++) {
            System.out.println(((Apple)apples.get(i)).id());
        }
    }
}

输出如下:

0
1
2

 二 泛型的特性

泛型只在编译阶段有效。看下面的代码:

import java.util.*;

public class Character {
    public static void main(String[] args) {
        List<String> stringArrayList = new ArrayList<String>();
        List<Integer> integerArrayList = new ArrayList<Integer>();
        
        Class classStringArrayList = stringArrayList.getClass();
        Class classIntegerArrayList = integerArrayList.getClass();
        
        if(classStringArrayList.equals(classIntegerArrayList)) {
            System.out.println("泛型测试:类型相同");
        }
    }

}

输出如下:

泛型测试:类型相同

通过上面的例子可以证明,在编译之后程序会采取去泛型化的措施。也就是说Java中的泛型,只在编译阶段有效。在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦出,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,泛型信息不会进入到运行时阶段。

对此总结成一句话:泛型类型在逻辑上看以看成是多个不同的类型,实际上是相同的基本类型

三 泛型类

泛型类型用于类的定义中,被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种容器类,如:List、Set、Map。

泛型类的最基本写法(这么看可能会有点晕,会在下面的例子中详解):

class 类名称 <泛型标识:可以随便写任意标识号,标识指定的泛型的类型>{
      private 泛型标识 /*(成员变量类型)*/ var; 
      .....

      }
    }

一个最普通的泛型类:

//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时,必须指定T的具体类型
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 static void main(String[] args) {        
        //泛型的类型参数只能是类类型(包括自定义类),不能是简单类型
        //传入的实参类型需与泛型的类型参数类型相同,即为Integer.
        Generic<Integer> genericInteger = new Generic<Integer>(123456);

        //传入的实参类型需与泛型的类型参数类型相同,即为String.
        Generic<String> genericString = new Generic<String>("key_vlaue");
        System.out.println("泛型测试:" + "key is " + genericInteger.getKey());
        System.out.println("泛型测试:" + "key is " + genericString.getKey());
        
    }
}

输出结果如下:

泛型测试:key is 123456
泛型测试:key is key_vlaue

定义的泛型类,就一定要传入泛型类型实参么?并不是这样,在使用泛型的时候如果传入泛型实参,则会根据传入的泛型实参做相应的限制,此时泛型才会起到本应起到的限制作用。如果不传入泛型类型实参的话,在泛型类中使用泛型的方法或成员变量定义的类型可以为任何的类型。

看一个例子:

        Generic generic = new Generic("111111");
        Generic generic1 = new Generic(4444);
        Generic generic2 = new Generic(55.55);
        Generic generic3 = new Generic(false);

        System.out.println("泛型测试:" + "key is " + generic.getKey());
        System.out.println("泛型测试:" + "key is " + generic1.getKey());
        System.out.println("泛型测试:" + "key is " + generic2.getKey());
        System.out.println("泛型测试:" + "key is " + generic3.getKey());

输出如下:

泛型测试:key is 111111
泛型测试:key is 4444
泛型测试:key is 55.55
泛型测试:key is false

注意:

  • 泛型的类型参数只能是类类型,不能是简单类型;
  • 不能对确切的泛型类型使用instanceof操作。如下面的操作是非法的,编译时会出错。
    if(ex_num instanceof Generic<Number>){   
    } 

四 泛型接口

泛型接口与泛型类的定义及使用基本相同。泛型接口常被用在各种类的生成器中,这是一种专门负责创建对象的类。一般而言,一个生成器只定义一个方法,该方法用以产生新的对象。在这里,就是next()方法。

可以看一个例子:

//定义一个泛型接口
public interface Generator<T> {
    public T next();
}

当实现泛型接口的类,未传入泛型实参时:

/**
 * 未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中
 * 即:class FruitGenerator<T> implements Generator<T>{
 * 如果不声明泛型,如:class FruitGenerator implements Generator<T>,编译器会报错:"Unknown class"
 */
class FruitGenerator<T> implements Generator<T>{
    @Override
    public T next() {
        return null;
    }
}

当实现泛型接口的类,传入泛型实参时:

/**
 * 传入泛型实参时:
 * 定义一个生产器实现这个接口,虽然我们只创建了一个泛型接口Generator<T>
 * 但是我们可以为T传入无数个实参,形成无数种类型的Generator接口。
 * 在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型
 * 即:Generator<T>,public T next();中的的T都要替换成传入的String类型。
 */
public class FruitGenerator implements Generator<String> {

    private String[] fruits = new String[]{"Apple", "Banana", "Pear"};

    @Override
    public String next() {
        Random rand = new Random();
        return fruits[rand.nextInt(3)];
    }
}

下面看一个具体的例子,CoffeeGenerator 是一个生成器类,实现Generator<Coffee>接口,它能够随机生成不同类型的Coffee对象:

import java.util.*;

///泛型接口,生成器
interface Generator<T>{
    T next();
}

class Coffee{
    private static long counter = 0;
    private final long id = counter++;
    public String toString() {
        return getClass().getSimpleName() + "  " + id;
    }
}

class Latte extends Coffee{}
class Mocha extends Coffee{}
class Cappuccino extends Coffee{}
class Americano extends Coffee{}
class Breve extends Coffee{}


//Coffee生成器(实现了Generator泛型接口、以及Iterable接口->可以用来迭代遍历生成器每个元素)
public class CoffeeGenerator implements Generator<Coffee>,Iterable<Coffee>{
    private Class[] types = {Latte.class,Mocha.class,Cappuccino.class,Americano.class,Breve.class};
    
    private static Random rand = new Random(47);
    
    public CoffeeGenerator() {}
    
    //设置生成器大小
    private int size = 0;
    public CoffeeGenerator(int sz) {size = sz;}

    //每次生成一个对象
    @Override
    public Coffee next() {
        // TODO Auto-generated method stub
        try {
            return (Coffee)types[rand.nextInt(types.length)].newInstance();
        } catch (InstantiationException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return null;
    }
    
    //实现Iterator接口(这里只是一个简单实现,很多细节没有处理),从而可以迭代遍历每个元素
    class CoffeeIterator implements Iterator<Coffee>{
        //保存剩余元素的个数
        int count = size;
                
        @Override
        public boolean hasNext() {
            // TODO Auto-generated method stub
            return count > 0;
        }

        @Override
        public Coffee next() {
            // TODO Auto-generated method stub
            count--;
            return CoffeeGenerator.this.next();
        }
        
        public void remove() {
            throw new UnsupportedOperationException();
        }
    }
    
    //用于创建迭代器对象
    @Override
    public Iterator<Coffee> iterator() {
        // TODO Auto-generated method stub
        return new CoffeeIterator();
    }
    
    public static void main(String[] args) {
        CoffeeGenerator gen = new CoffeeGenerator();
        for(int i=0;i<5;i++)
            System.out.println(gen.next());
        //循环语句之所以可以使用,只是因为实现了Iterable接口
        for(Coffee c:new CoffeeGenerator(5))
            System.out.println(c);
    }    
}

输出如下:

Americano  0
Latte  1
Americano  2
Mocha  3
Mocha  4
Breve  5
Americano  6
Latte  7
Cappuccino  8
Cappuccino  9

参数化的Generator接口确保next()的返回值是参数的类型。CoffeeGenerator同时还实现了Iterable接口,所以它可以在循环语句中使用。不过,它还需要一个"末端哨兵"来判断何时停止,这正是第二个构造器的功能。

参考文章

[1]Java编程思想

[2]java 泛型详解-绝对是对泛型方法讲解最详细的,没有之一(部分转载)

[3]java 泛型详解

[4]Java中的泛型方法

[5]java泛型详解

猜你喜欢

转载自www.cnblogs.com/zyly/p/10762448.html