Java泛型总结

Java泛型总结

Java泛型是JDK5引入的一个新特性,允许在定义类和接口的时候使用类型参数(type parameter)。声明的类型参数在使用的时候使用具体的类型来替换。泛型最主要的应用是在JDK5中的新集合类框架中。对于泛型概念的引入,开发社区的观点是褒贬不一。从好的方面上说,泛型的引入可以解决之前的集合类框架在使用过程中通常会出现的运行时刻类型错误,因为编译器可以在编译时刻就发现很多明显的错误。从不好的方面说,为了保证与旧版本的兼容性,Java泛型的实现上还存在着不够优雅的地方。

类型擦除

正确理解泛型概念的首要前提是理解类型擦除(type erasure)。Java中的泛型基本上都是在编译器这个层次来实现的。在生成的Java字节码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉。这个过程就称为类型擦除。比如在代码中定义的List<Object>List<String>等类型,在编译之后都会变成ListJVM看到的只是List,而由泛型附加的类型信息对JVM来说是不可见的。Java编译器会在编译时尽可能的发现可能出错的地方,但是仍然无法避免在运行时刻出现类型转换异常的情况。

通过如下代码片段感受类型擦除:

        ArrayList<String> a1 = new ArrayList<>();ArrayList<Integer> a2 =new ArrayList<>();Class c1 =a1.getClass();Class c2 = a2.getClass();System.out.println(c1.equals(c2));    //Output: true

此时,程序输出true,这就是类型擦除造成的。因为不管是ArrayList<String>还是ArrayList<Integer>,都会在编译期被编译器擦除成ArrayList。编译器这么做的原因归根结底还是为了兼容JDK5前未使用泛型的代码,因此不得不让编译器擦除有关类型信息的部分,这样生成的代码其实就是类型无关的。

        List<Integer> list = new ArrayList<>();Map<String,Integer> map = new HashMap<>();System.out.println(Arrays.toString(list.getClass().getTypeParameters()));    //[E]System.out.println(Arrays.toString(map.getClass().getTypeParameters()));    //[K, V]

我们期望的是返回泛型参数的类型,结果返回的仅仅是参数的占位符。

    public static <T> T[] makeArray(){return new T[10];    //编译期报错:不能创建泛型类型的数组}

因为T仅仅是个占位符,并不具有真实的类型信息。为了解决这个问题,可以利用反射:

    public static <T> T[] makeArray(Class<T> clazz) {return (T[]) Array.newInstance(clazz, 10);}

很多泛型的奇怪特性都与类型擦除的存在有关,包括:

  • 泛型类并没有自己独有的Class类对象。比如并不存在List<String>.class或是List<Integer>.class,而只有List.class

  • 静态变量是被泛型类的所有实例所共享的。对于声明为MyClass<T>的类,访问其中的静态变量的方法仍然是MyClass.myStaticVar。不管是通过new MyClass<String>还是new MyClass<Integer>创建的对象,都是共享一个静态变量。

  • 泛型的类型参数不能用在Java异常处理的catch语句中。因为异常处理是由JVM在运行时刻来进行的。由于类型信息被擦除,JVM是无法区分两个异常类型MyException<String>MyException<Integer>的。对于JVM来说,它们都是MyException类型的,也就无法执行与异常对应的catch语句。

当执行类型擦除时,首先是找到用来替换类型参数的具体类。这个具体类一般是Object。如果指定了类型参数的上界的话,则使用这个上界。把代码中的类型参数都替换成具体的类。同时去掉出现的类型声明,即去掉<>的内容。比如T get()方法声明就变成了Object get()List<String>就变成了List。接下来就可能需要生成一些桥接方法(bridge method),这是由于擦除了类型之后的类可能缺少某些必须的方法。比如考虑下面的代码:


class MyString implements Comparable<String> {public int compareTo(String str) {return 0;}
}

当类型信息被擦除之后,上述类的声明变成了class MyString implements Comparable。但是这样类MyString就会有编译错误,因为没有实现接口Comparable声明的compareTo(Object)方法。这个时候就由编译器来动态生成这个方法。

实例分析

了解类型擦除机制之后,就会明白编译器承担了全部的类型检查工作。编译器禁止某些泛型的使用方式,正是为了确保类型的安全性。以List<Object>List<String>为例来具体分析:

public void inspect(List<Object> list) {for(Object obj : list) {System.out.println(obj);}list.add(1);    //这个操作在当前方法的上下文是合法的
}public void test() {List<String> strs = new ArrayList<String>();inspect(strs);    //编译错误
}

这段代码中,inspect方法接受List<Object>作为参数,当在test方法中试图传入List<String>的时候,会出现编译错误。假设这样的做法是允许的,那么在inspect方法中就可以通过list.add(1)来向集合中添加一个数字。这样在test方法看来,其声明为List<String>的集合中被添加了一个Integer类型的对象,这显然是违反类型安全原则的,在某个时候肯定会抛出ClassCastException。因此,编译器禁止这样的行为。

通配符与上下界

在使用泛型类的时候,既可以指定一个具体的类型,如List<String>就声明了具体的类型是String;也可以用通配符?来表示未知类型,如List<?>就声明了List中包含的元素类型是未知的。通配符所代表的其实是一组类型,但具体的类型是未知的。List<?>所声明的就是所有的类型都是可以的。但是List<?>并不等同于List<Object>List<Object>实际上确定了List中包含的是Object及其子类,在使用的时候可以通过Object来进行引用。而List<?>则表示其中所包含的元素类型是不确定。其中可能包含的是String,也可能是Integer。如果它包含了String的话,往里面添加Integer类型的元素就是错误的。正因为类型未知,就不能通过new ArrayList<?>()的方法来创建一个新的ArrayList对象。因为编译器无法知道具体的类型是什么。但是对于List<?>中的元素总是可以用Object来引用的,因为虽然类型未知,但肯定是Object及其子类。考虑下面的代码:

public void wildcard(List<?> list) {list.add(1);    //编译错误
}

如上所示,试图对一个带通配符的泛型类进行操作的时候,总是会出现编译错误。其原因在于通配符所表示的类型是未知的。

因为对于List<?>中的元素只能用Object来引用,在有些情况下不是很方便。在这些情况下,可以使用上下界来限制未知类型的范围。如List<? extends Number>说明List中包含的是Number及其子类。而List<? super Number>则说明List中包含的是Number及其父类。当引入了上界时候,在使用类型的时候就可以使用上界类中定义的方法。比如访问List<? extends Number>的时候,就可以使用Number类的intValue等方法。

类型系统

Java中,比较常见的是通过继承机制而产生的类型体系结构。比如String继承自Object。根据Liskov替换原则,子类是可以替换父类的。当需要Object类的引用的时候,如果传入一个String对象是没有任何问题的。但是反过来的话,即用父类的引用替换子类引用时,就需要进行强制类型转换。编译器并不能保证运行时刻的这种转换一定是合法的。这种自动的子类替换父类的转换机制,对于数组也是适用的。String[]可以替换Object[]。但是泛型的引入,对于这个类型系统产生了一定的影响。例如List<String>是不能替换List<Object>的。

引入泛型之后的类型系统增加了两个维度:一个是类型参数自身的继承体系结构,另外一个是泛型类或接口自身的继承体系结构。第一个指的是对于List<String>List<Object>这样的情况,类型参数String继承自Object。而第二种指的是List接口继承自Collection接口。对于这个类型系统,有如下的一些规则:

  • 相同类型参数的泛型类的关系取决于泛型类自身的继承体系结构。即List<String>Collection<String>的子类型,List<String>可以替换Collection<String>。这种情况也适用于带有上下界的类型声明。

  • 当泛型类的类型声明中使用了通配符的时候,其子类可以在两个维度上分别展开。如对Collection<? extends Number>来说,其子类型可以在Collection这个维度上展开,即List<? extends Number>Set<? extends Number>等;也可以在Number这个维度展开,即Collection<Double>Collection<Integer>等。如此循环下去,ArrayList<Long>HashSet<Double>等也都算是Collection<? extends Number>的子类型。

  • 如果泛型类中包含多个类型参数,则对每个类型参数分别应用上面的规则。

因此,对于上面错误的代码,只需要将List<Object>修正为List<?>即可。List<String>List<?>的子类型。

开发自己的泛型类

泛型类与一般的Java类基本相同,只是在类和接口定义上多出来了用<>声明的类型参数。一个类可以有多个类型参数,比如MyClass<X, Y, Z>。每个类型参数在声明的时候可以指定上下界。所声明的类型参数在Java类中可以像一般的类型一样作为方法的参数和返回值,或是作为域和局部变量的类型。由于类型擦除机制,类型参数并不能用来创建对象或是作为静态变量的类型。考虑下面的泛型类中的正确和错误的用法。

class ClassTest<X extends Number, Y, Z> {private X x;private static Y y;    //编译错误,不能用在静态变量中public X getFirst() {return x;    //正确用法}public void wrong() {Z z = new Z();    //编译错误,不能查创建对象}
}

假设允许类型参数声明为静态属性,那么如下代码将会非常混乱。

public class Computer<T> {private static T os;public Computer(T os) {this.os = os;}public T getOS() {return os;}public static void main(String [] args) {Computer<Linux> c1 = new Computer<>();Computer<MacOS> c2 = new Computer<>();Computer<Windows> c3 = new Computer<>();System.out.println(c1.getOS());System.out.println(c2.getOS());System.out.println(c3.getOS());}
}

因为osComputer类的静态属性,所以c1c2c33Computer实例共享这个属性,那么此时os的类型是什么?因此,不允许声明静态的类型参数属性。

总结

在使用Java泛型的时候可以遵循一些基本的原则,从而避免一些常见的问题。

  • 在代码中避免泛型类和原始类型的混用。比如List<String>List不应该共同使用。这样会产生一些编译器警告和潜在的运行时异常。

  • 在使用带通配符的泛型类的时候,需要明确通配符所代表的一组类型的概念。由于具体的类型是未知的,很多操作是不允许的。

  • 泛型类最好不要同数组一块儿使用。只能创建new List<?>[10]这样的数组,无法创建new List<String>[10]这样的。这限制了数组的使用能力,而且会带来很多费解的问题。

参考

  • InfoQ

  • Java泛型:类型擦除

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/292761.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

mysql查看日志命令_面对成百上千台服务器产生的日志,试试这款轻量级日志搬运神器!

之前我们搭建的ELK日志收集系统&#xff0c;主要是用来收集SpringBoot应用的日志。其原理是应用通过Logstash插件&#xff0c;使用TCP向Logstash传输日志&#xff0c;从而存储到Elasticsearch中去。但是有很多中间件的日志都是直接存储在文件中的&#xff0c;比如Nginx、Elasti…

单元测试 | 如何Mock IHttpClientFactory

前言编写单元测试时&#xff0c;常常需要使用Mock框架&#xff08;例如Moq&#xff09;生成测试类的依赖接口的"模拟"实现&#xff0c;并验证接口是否按预期使用&#xff1a;_mediatorMock new Mock<IMediator>();_mediatorMock.Setup(x > x.Send(It.IsAny…

Wireshark和 TcpDump抓包分析心得

1. Wireshark与tcpdump介绍 Wireshark是一个网络协议检测工具&#xff0c;支持Windows平台和Unix平台&#xff0c;我一般只在Windows平台下使用Wireshark&#xff0c;如果是Linux的话&#xff0c;我直接用tcpdump了&#xff0c;因为我工作环境中的Linux一般只有字符界面&#x…

遇到可爱女生如何搭讪?

1 旺仔落泪▼。。。2 养狗一日&#xff0c;用狗一时&#xff08;素材来源网络&#xff0c;侵删&#xff09;▼3 试图混进芒果里的鹦鹉&#xff08;素材来源网络&#xff0c;侵删&#xff09;▼4 把滴滴司机的坐垫沾走了▼5 如何高效记单词&#xff08;素材来源网络&#xf…

Tomcat 上添加虚拟相对路径

为什么80%的码农都做不了架构师&#xff1f;>>> <Context docBase"D:/SHARP" path"/test" reloadable"true" /> 转载于:https://my.oschina.net/zz006/blog/754631

198道K8sDocker面试真题大汇总,全网最全八股!

最近看到两个数据&#xff0c;想跟你们分享一下&#xff1a;一是&#xff0c;目前云原生开发人员已经达680万&#xff08;SlashData数据&#xff09;&#xff1b;二是&#xff0c;云和容器技术首超Linux&#xff0c;成最受青睐技能&#xff08;Linux基金会数据&#xff09;。云…

iptables 手册

iptables防火墙可以用于创建过滤(filter)与NAT规则。所有Linux发行版都能使用iptables&#xff0c;因此理解如何配置iptables将会帮助你更有效地管理Linux防火墙。如果你是第一次接触iptables&#xff0c;你会觉得它很复杂&#xff0c;但是一旦你理解iptables的工作原理&#x…

Android之如何看混淆后的错误日志代码

第一步&#xff1a; 找到混淆的mapping.txt文件 build -> outputs -> mapping -> release -> mapping.txt 或者&#xff1a;1项目目录的progurad下有一个mapping.txt文件 这是混淆后的名字 和原名字的映射关系。 第二步: 找到项目用的sdk依次定位到sdk -> to…

男朋友的回答可以多敷衍?

1 跟最好的朋友好到什么程度&#xff08;素材来源网络&#xff0c;侵删&#xff09;▼2 不要在妈妈打麻将的时候要生活费&#xff08;素材来源网络&#xff0c;侵删&#xff09;▼3 现在卖家还要发毒誓了&#xff08;素材来源网络&#xff0c;侵删&#xff09;▼4 刘德华老师…

学习Java,容易被你忽略的小细节(2)

昨天心情真的太糟糕了&#xff0c;写完《学习Java&#xff0c;值得注意你注意的问题&#xff08;1&#xff09;》之后&#xff0c;迎来些许的支持以后就是一片片的谴责。我的主页上涌现出许许多多Java方面的牛人&#xff0c;谴责我水平太低&#xff0c;写的问题太初级。搞得我非…

controller是什么意思_SpringMVC是什么??

SpringMVC是什么&#xff1f;一&#xff0c;首先是一个MVC框架。在web模型中&#xff0c;MVC是一种很流行的框架&#xff0c;通过把Model&#xff0c;View&#xff0c;Controller分离&#xff0c;把较为复杂的web应用分成逻辑清晰的几部分&#xff0c;是为了简化开发&#xff0…

You third iOS app,这个APP要放到手机上面运行,才会成功,才会新建记录。

当提示如上图所示时&#xff0c;很可能你只有在苹果设备上运行才可以提示成功&#xff0c; iCloud is available。 转载于:https://www.cnblogs.com/liqiwa/p/5933542.html

客户要求ASP.NET Core API返回特定格式,怎么办?(续2)

前言前2次&#xff0c;我们都是假设客户需要返回不同的字符串格式。但是&#xff0c;有可能客户要求的返回是非本文格式&#xff0c;比如文件流。怎么办&#xff1f;思路前提&#xff0c;当然还是使用同一API接口&#xff0c;不影响现有使用方式。虽然ASP.NET Core Web API默认…

第四章例题、心得及问题。

例题4-1&#xff1a; #include<stdio.h> #include<math.h> int main(void) {int denominator,flag;double item,pi;flag1;denominator1;item1.0;pi0;while(fabs(item)>0.0001){itemflag*1.0/denominator;pipiitem;flag-flag;denominatordenominator2;}pipi*4;pr…

springboot springcloud区别_SpringCloud微服务全家桶-第一篇!为什么要用微服务

从今天开始&#xff0c;学习SpringCloud微服务全家桶。一、引导1、什么是微服务&#xff1f;2、微服务之间是如何独立通讯的3、springCloud和Dubbo有哪些区别&#xff1f;4、什么是服务熔断&#xff1f;什么是服务降级5、微服务的优缺点分别是什么&#xff1f;6、微服务技术栈有…

C#之Lock

lock 关键字将语句块标记为临界区&#xff0c;方法是获取给定对象的互斥锁&#xff0c;执行语句&#xff0c;然后释放该锁。 class Program{static void Main(string[] args){Thread t new Thread(LockObject.MonitorIncrement);Thread t1new Thread(new ThreadStart(LockObje…

记一次 .NET 某消防物联网 后台服务 内存泄漏分析

一&#xff1a;背景 1. 讲故事去年十月份有位朋友从微信找到我&#xff0c;说他的程序内存要炸掉了。。。截图如下&#xff1a;时间有点久&#xff0c;图片都被清理了&#xff0c;不过有点讽刺的是&#xff0c;自己的程序本身就是做监控的&#xff0c;结果自己出了问题&#xf…