深入分析java中的泛型机制

简介: 想要学好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>,既然是类型参数列表,也就是说可以有多个类型参数,比如<E,T>。我们直接创建一个泛型类看看吧。

//这里的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<E,T>。这里的<E,T>我们还可以添加任意多个。他就像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、泛型接口


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

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> T genericMethod(T t){
       return t;
}

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

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

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

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)<? extends E> 表示类型的上界是E,只能是E或者是E的子孙类。 (3)<? super E> 声明了类型的下界E,只能是E或者是E的父类。

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

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


5、类型擦除


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


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

v2-40657d6dc2df6b8e39fc47f8c08b8743_1440w.jpg

上面我们定义了这两个代码会出现这样的问题,这是因为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,泛型就先到这里。

相关文章
|
16天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
45 2
|
20天前
|
Java 编译器
探索Java中的异常处理机制
【10月更文挑战第35天】在Java的世界中,异常是程序运行过程中不可避免的一部分。本文将通过通俗易懂的语言和生动的比喻,带你了解Java中的异常处理机制,包括异常的类型、如何捕获和处理异常,以及如何在代码中有效地利用异常处理来提升程序的健壮性。让我们一起走进Java的异常世界,学习如何优雅地面对和解决问题吧!
|
19天前
|
Java 数据库连接 开发者
Java中的异常处理机制及其最佳实践####
在本文中,我们将探讨Java编程语言中的异常处理机制。通过深入分析try-catch语句、throws关键字以及自定义异常的创建与使用,我们旨在揭示如何有效地管理和响应程序运行中的错误和异常情况。此外,本文还将讨论一些最佳实践,以帮助开发者编写更加健壮和易于维护的代码。 ####
|
25天前
|
安全 IDE Java
Java反射Reflect机制详解
Java反射(Reflection)机制是Java语言的重要特性之一,允许程序在运行时动态地获取类的信息,并对类进行操作,如创建实例、调用方法、访问字段等。反射机制极大地提高了Java程序的灵活性和动态性,但也带来了性能和安全方面的挑战。本文将详细介绍Java反射机制的基本概念、常用操作、应用场景以及其优缺点。 ## 基本概念 ### 什么是反射 反射是一种在程序运行时动态获取类的信息,并对类进行操作的机制。通过反射,程序可以在运行时获得类的字段、方法、构造函数等信息,并可以动态调用方法、创建实例和访问字段。 ### 反射的核心类 Java反射机制主要由以下几个类和接口组成,这些类
57 2
|
29天前
|
存储 Java 关系型数据库
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践,包括连接创建、分配、复用和释放等操作,并通过电商应用实例展示了如何选择合适的连接池库(如HikariCP)和配置参数,实现高效、稳定的数据库连接管理。
52 2
|
1月前
|
Java 关系型数据库 数据库
面向对象设计原则在Java中的实现与案例分析
【10月更文挑战第25天】本文通过Java语言的具体实现和案例分析,详细介绍了面向对象设计的五大核心原则:单一职责原则、开闭原则、里氏替换原则、接口隔离原则和依赖倒置原则。这些原则帮助开发者构建更加灵活、可维护和可扩展的系统,不仅适用于Java,也适用于其他面向对象编程语言。
22 2
|
26天前
|
Java 开发者
深入理解Java异常处理机制
【10月更文挑战第29天】在Java的世界中,异常处理如同生活的调味品,不可或缺。它确保了程序在遇到错误时不会崩溃,而是优雅地继续运行或者给出提示。本文将带你领略异常处理的奥秘,从基础的try-catch语句到高级的自定义异常,让你在面对程序中的各种“意外”时,能够从容应对。
|
28天前
|
SQL Java
探索Java中的异常处理机制
【10月更文挑战第26天】 在本文中,我们将深入探讨Java编程语言的异常处理机制。通过分析不同类型的异常、异常的捕获与抛出方式,以及如何自定义异常类,读者将能够更好地理解并应用Java中的异常处理机制来提高代码的健壮性和可读性。
24 0
|
13天前
|
Java 开发者
Java多线程编程中的常见误区与最佳实践####
本文深入剖析了Java多线程编程中开发者常遇到的几个典型误区,如对`start()`与`run()`方法的混淆使用、忽视线程安全问题、错误处理未同步的共享变量等,并针对这些问题提出了具体的解决方案和最佳实践。通过实例代码对比,直观展示了正确与错误的实现方式,旨在帮助读者构建更加健壮、高效的多线程应用程序。 ####
|
4天前
|
缓存 Java 开发者
Java多线程编程的陷阱与最佳实践####
本文深入探讨了Java多线程编程中常见的陷阱,如竞态条件、死锁和内存一致性错误,并提供了实用的避免策略。通过分析典型错误案例,本文旨在帮助开发者更好地理解和掌握多线程环境下的编程技巧,从而提升并发程序的稳定性和性能。 ####