想要学好java,泛型机制是一个必须要掌握的知识点,无奈很多书上写的不是很啰嗦,就是概念太多难以理解,因此参考了很多篇文章,对其进行整理了一下,希望对你有所帮助。

一、认识泛型

1、为什么要引入泛型?

泛型其实是在jdk1.5中才添加的。在jdk1.5之前我们要创建一个容器对象,是这样往里面添加内容的。

List list = new ArrayList();
list.add("我是字符串");//可以添加字符串
list.add(10.67);//可以添加float
list.add(false);//可以添加boolean

也就是说我们创建了一个容器之后,我们可以往里面添加任何东西,这时候就麻烦了,如果我们只想保存字符串,但是一不小心存了一个int类型的值,在输出的时候肯定会报错误的。那怎么办呢?于是乎,在jdk1.5添加了泛型机制,去规范我们输入的值。

List<String> list = new ArrayList<String>();

这时候我们的list就只能保存String类型的值了,如果我们保存了int类型的值,那么就会在编译期报错(一般情况下在ide写代码的时候,就会自动编译)。

2、泛型概念

有了上面这个例子,我们再来理解一下泛型的概念:

泛型实现了了参数化类型的概念,使得代码可以应用于多种类型。

那什么是参数化类型呢?也就是说把我们要操作的数据类型保存为一个参数。比如下面这样的

List<E>, Queue<E>

我们把要操作的数据类型变成了一个“E”。这个E就是一个类型参数,我们可以指定E是具体String类型,也可以指定一个通配符,表示可以操作一类数据类型。

3、使用泛型的优点

在java中,官方强烈推荐我们使用泛型。就是因为他有很多优点。

(1)类型安全:我们在使用泛型之后,可以指定输入的类型,比如只能输入String类型的值,输入其他的就会报错,这在代码编写时,为我们提供了极大的方便。

(2)消除强制类型转换:也就是说我们不需要进行类型转化,直接存储、直接输出。

(3)只在编译器有效:也就是说在运行时泛型是无效的。这避免了jvm花费时间在运行时做额外的操作。

对于第三点,我们这里去验证一下(这里使用到了最基本的反射方法):

public class Test {
    public static void main(String[] args) throws Exception {
        //第一个list1我们只创建了一个容器:可以输入任何类型
        ArrayList list1=new ArrayList();
        //第二个list2我们创建了一个泛型:只能输入String类型
        ArrayList<String> list2=new ArrayList<String>();
        //使用反射机制,获取Class
        Class c1=list1.getClass();
        Class c2=list2.getClass();
        //疑问:在运行时,他们俩相等嘛?
        System.out.print(c1==c2);
    }
}

在第三点其实已经给出答案了,输出肯定是true。因为泛型只在编译器有效,在运行时期无效,也就变成了一样的。就好比,在编译时期一个是羊,一个是披着狼皮的羊,在外表看着不一样。在运行时期,把狼皮脱掉了。就全暴露了,就都是羊了。

目前为止,我们已经把泛型的产生的原因(这只是原因之一),泛型的概念以及泛型的优点说出来了,下面我们就来看看,泛型机制在java中是如何使用的。

二、泛型的使用

泛型的使用主要是在三个方面,泛型类、泛型接口、泛型方法。我们一个一个去看。

1、泛型类

泛型类的使用也是非常简单的,和普通类的区别就是类名后有类型参数列表,既然是类型参数列表,也就是说可以有多个类型参数,比如。我们直接创建一个泛型类看看吧。

//这里的E和T,可以有任意多个,名字使我们自己定的
public class Generic<E,T>
    //这里的E和T由外部指定  
    private T key;
    private E e;
    public Generic(E e,T key) 
        this.e=e;
        this.key = key;
    }
    //我们使用E和T就像使用String这些一样
    public T getKey()
        return key;
    }
    public E getE()
        return e;
    }
}

我们会发现,其实泛型类和普通类的区别也就是有了一个参数类型列表:Generic。这里的我们还可以添加任意多个。他就像String,Integer等等类型一样。名字是我们取的。使用的时候,也是和String、Integer这些一样。

下面我们就使用一下这个泛型类

public class Test {
    public static void main(String[] args) {
        Generic<Integer,String> generic=new Generic<Integer,String>(123"test");
        System.out.println(generic.getE());
        System.out.println(generic.getKey());
    }
}
//输出:
//test
//123

在使用这个泛型类的时候,有几个地方需要我们去注意:

(1)实例化泛型类时,必须指定E和T的具体类型,比如这里指定的是Integer和String

(2)指定的具体类型必须是类,不能是int,float等这些基础类型

(3)不能对泛型类使用instanceof。为什么呢?这是因为泛型类只在编译期有效,在运行时期不区分是什么类型,也就是在上面说的,穿着狼皮的羊脱掉狼皮之后,两只羊就都一样了。比如下面的代码是不合法的。

User<Integer> integerUser = new User<Integer>();
if(integerUser instanceof User<Integer>){ }
//会出现以下错误提示
//Cannot perform instanceof check against parameterized type Box<Integer>. 
//Use the form Box<?> instead since further 
//generic type information will be erased at runtime

2、泛型接口

泛型接口其实和泛型类一样,和普通接口的区别也是后面添加了类型参数列表。我们先创建一个泛型接口来看看。

public interface GenericInterface<T{
    //定义一个普通方法:参数是E和T
    //注意:这可不是泛型方法
    public void test(T t) ;
}

注意:在泛型接口里面我们只是定义了一个普通的方法,可不是泛型方法,然后我们就可以使用一般的接口那样使用泛型接口了。

//GenericInterface<String>需要指定具体的类型String
public class GenericTest  implements GenericInterface<String>{
    //泛型接口中
    @Override
    public void test(String name) {
        System.out.println("具体类型是:String:"+name);
    }
    public static void main(String[] args) {
        GenericTest genericTest = new GenericTest();
        genericTest.test("泛型接口");
    }
}

在使用泛型接口时候和使用泛型类一样同样有几个点需要我们知道:

(1)继承泛型接口的时候就需要指定具体是什么类型

(2)泛型中的方法也需要对相应的泛型参数赋予具体的类型。

3、泛型方法

泛型方法是什么意思呢?也就是我们输入参数的时候,输入的是泛型参数,而不是具体的参数。我们在调用这个泛型方法的时候,需要对泛型参数实例化。我们还是直接看例子:

//定义了一个泛型方法
public <T> genericMethod(T t){
       return t;
}

这里最重要的就是public后面的,只有有了这个东西才称得上泛型方法。当然这里的也是一个泛型化列表。可以是。我们给出几个普通方法,对比一下区别所在:

//1、public后面没有<T>
public T getName(T t)
    return t;
}
//2、就是和普通方法一样
public String getName(String  b) {
    return b;
}
//3、错误的泛型方法
public <T> getName(Generic<E> e){
     //错误原因是因为E未声明,我们不知道
}  

现在我们知道区别了吧,也就是说泛型方法的标志就是,权限修饰符后面的。我们看一下如何去使用。

public class GenericTest {    
    public static void main(String[] args) {
        Generic genericTest = new Generic();
        String a=genericTest.genericMethod("这里可以是任意类型");
        int b=genericTest.genericMethod(123);
        double c=genericTest.genericMethod(12.34);
    }
}

我们可以像普通方法那样去使用即可。

注意:在静态方法中使用泛型参数的时候,需要我们把静态方法定义为泛型方法

//比如说:我们想在静态方法getName中使用泛型参数T
public static void getName(T t){
    //这种是错误的,我们需要把静态方法转变成泛型方法。
}
public static <T> void getName(T t){
    //这样就可以了
}

4、泛型通配符

其实泛型通配符严格的划分是属于泛型类一部分的,为什么要用到泛型通配符呢?因为有时候我们希望传入的类型在一个指定的范围内。举个例子,之前我们传入的类型必须指定为Integer类型的,但是后来业务变了,Integer的父类Number类也可以传入。这时候就需要用到泛型通配符了。

泛型中有三种通配符形式:

(1) 无限制通配符:表示我们可以传入任意类型的参数
(2) 表示类型的上界是E,只能是E或者是E的子孙类。
(3) 声明了类型的下界E,只能是E或者是E的父类。

我们使用代码举个例子相信你就会明白了。

//在这里我们传入Number或者是Number的子类都可以
private <T extends Number> getName(T t){
    return t;
}
//在这里我们传入E或者是E的父类都可以
private <E> add(List<? super E> e){
    return e;
}

5、类型擦除

我们在文章一开始就曾经说过,泛型只在编译期有效,在运行期虚拟机是分辨不出来的,而且我们还用反射机制来验证了一下,发现在运行期两个ArrayList确实是一样的。那么问题来了,从编译期能够识别泛型,再到运行期不能识别泛型肯定需要一个过程,在这个过程中编译器肯定要对泛型进行一个处理,才能到运行期。这个处理就是类型擦除。

也就是说,在编译时期java编译器就完成了类型擦除。我们可以先看下面一种情况:

图片

上面我们定义了这两个代码会出现这样的问题,这是因为java编译器在编译时期就进行了类型擦除,擦出了之后发现两个方法的方法名、参数列表一样。于是出现了两个一样的方法,报了这个错误。

上面出现的这种情况对我们来说真的是太麻烦了,如何解决这个问题呢?java又为我们提供了一个机制:边界,来解决这个问题。什么意思呢?之前我们的类型擦除,都是直接擦除到Object,现在有了边界之后,我们只擦出到一定的界限就不擦出了。我们再来看下面的使用了边界之后的好处:

public class GenericTest {
    interface A {
        void testA();
    }
    interface B{
        void testB();
    }
    public static class Test<T extends A & B>{
        private T val;
        public Test(T val){
            val = val;
        }
        public void test(){
            val.testA();
            val.testB();
        }
    }
}

现在应该能看明白了,我们限定了类型擦除的边界之后,就不会出现这种错误了。编译器会把类型参数替换为第一个边界。如果你还不明白,就动手操作一遍。

三、泛型总结

如果我们之前了解过java中的语法糖的知识话,我们应该知道其是泛型就是一个语法糖,语法糖就是一个方便程序员的功能,对语言没有任何影响。真正想要掌握泛型机制的话,还需要自己动手对每一块内容自己写一遍。OK,泛型就先到这里。


更多相关文章

  1. 设计模式之模板方法模式
  2. 面试必问:String类型为什么设计成不可变的?
  3. Java实现定时任务的三种方法
  4. 服务端开发指南与最佳实战 | 数据存储技术 | MySQL(01)数据类型的
  5. 基本类型转 String
  6. Java 为什么需要保留基本数据类型
  7. 使用 ThreadLocal 变量的时机和方法
  8. Java 中关于 String 类型的 10 个问题
  9. clone 方法是如何工作的

随机推荐

  1. golang不定长参数写法
  2. go和golang之间有区别吗?
  3. golang read会阻塞么
  4. golang并发不是并行
  5. golang panic可以捕获标准错误吗
  6. golang和c语言的区别是什么?
  7. golang中vendor什么时候进来的
  8. golang map需要make吗
  9. golang怎么生成不重复随机数
  10. golang slice如何拷贝