Java数据结构与算法分析-第一章(引论)-Java中的范型<T,E>构件
一、为什么需要使用范型?
官方的说法是:Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。
泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
说明一下:在没有范型或者不使用范型的时候,下面给出一个假设:
Person类的构造器:public Person(Object o){this.o=o}
我们编码的时候实例化一个类的实例(含参数):Person p1=new Person(1), Person p1=new Person("小风微凉");
那么问题来了,我们知道传入的参数被传递给了Person的o属性,由于传入的类型都是Object的子类型,所以编译的时候是没有问题的,此时看不到错误,但是我们 并不知道这个属性o会做如何的处理,这个处理就会对数据的类型有着严格的要求了,当然处理的时候也不会编译报错,毕竟是Object类型,但是会在jvm调用执行的时候报错:ClassCastException
所以:使用范型的好处之一就是:把报错的时机,从运行时提前到了编译时,这样更有利于我们编码。
并且,范型具有自动装箱拆箱的功能。
二、举个例子分析一下
不使用范型的例子就太多了,这里我直接使用范型,在使用中说明一下我的发现。
(1)定义一个范型接口:GenericInterface<T>:这种用法,在前一篇介绍ArrayList源码的时候就多次出现过。
package com.xfwl.generics;
/**
* 数据结构与算法分析:第一章
* @function 泛型结构(jre1.5及以上)
* @author 小风微凉
* @time 2018-5-6 下午12:51:45
* @param <T>
*/
public interface GenericInterface<T> {
//定义一个属性
public static int INT_1=1;
/**
* 接口中定义的范型方法
* @param t 传入参数范型类型T
* @return 返回范型类型T
*/
public T fun_1(T t);
}
说明一下:
在定义这个范型接口的时候,我发现定义属性的时候:不能使用范型!
原因:因为接口中的属性的默认修饰符为:public static ,而Java的泛型是擦除机制,所以不能被static修饰为共有部分,因此编译不会通过。
(2)定义一个范型类:GenericClass<T,E>
package com.xfwl.generics;
/**
* 数据结构与算法分析:第一章
* @function 泛型类(jre1.5及以上)
* @author 小风微凉
* @time 2018-5-6 下午12:39:08
*/
@SuppressWarnings("unchecked")
public class GenericClass<T,E> implements GenericInterface<T> {
private T att1;//属性1
public String att2;//属性2
private GenericClass generic=null;
public GenericClass(T t){
this.att1=t;
}
//单例模式-懒汉式:范型不能用于static修饰的属性或方法中
public GenericClass getSingleInstence(T t){
if(generic==null){
generic=new GenericClass<T,E>(t);
}
return generic;
}
/**
* 接口中实现方法
*/
public T fun_1(T t) {
System.out.println("fun_1()输出:"+t);
return null;
}
/**
* 当前类中的方法
* @param e 传递过来的类型
* @return 返回范型E
*/
public E fun_2(E e){
System.out.println("fun_2()输出:"+e);
return e;
}
}
说明一下:
定义范型类的时候,可以直接实现范型接口:implements GenericInterface<T>,而泛型类中的范型也可允许为多个即: GenericClass<T,E>,但是需要注意的一点是:范型类中的范型必须包含范型接口中的范型,原因很简单,既然实现了接口,就必须重写接口里面的方法,而这些方法就使用了范型接口中的范型,所以就这样喽!同样的,范型类中不可以使用static去修饰范型属性。
(3)范型的使用
定义了一个使用范型的类:
package com.xfwl.generics;
public class Test {
/**
* @param args
*/
public static void main(String[] args) {
//不使用范型
GenericClass g_4=new GenericClass("type_String");
//使用范型:三种方法均可
GenericClass<String, Integer> g_1=new GenericClass<String, Integer>("type_String");
GenericClass<String, Integer> g_2=new GenericClass("type_String");
GenericClass<String, Integer> g_3=new GenericClass<>("type_String");//jre1.7及以上:菱形运算符
//开始调用方法
g_1.fun_1("g_1_1");//jre1.5及以上
g_1.fun_2(1); g_2.fun_1("g_2_1");//jre1.5及以上
g_2.fun_2(2); g_3.fun_1("g_3_1");//jre1.7及以上
g_3.fun_2(3); }
}
先运行一下,看下运行结果:
fun_1()输出:g_1_1
fun_2()输出:1
fun_1()输出:g_2_1
fun_2()输出:2
fun_1()输出:g_3_1
fun_2()输出:3
说明一下:
上面三种方法都可以拿到范型类的对象,并且都可以正常运行。
那么来看看,这三个对象在调用范型类的范型方法的时候,编译器是如何处理的!!!
(1)使用范型,编译器是如何处理的?
//使用范型:三种方法均可
GenericClass<String, Integer> g_1=new GenericClass<String, Integer>("type_String");
GenericClass<String, Integer> g_2=new GenericClass("type_String");
GenericClass<String, Integer> g_3=new GenericClass<>("type_String");//jre1.7及以上:菱形运算符
//开始调用方法
g_1.fun_1("g_1_1");//jre1.5及以上
g_1.fun_2(1); g_2.fun_1("g_2_1");//jre1.5及以上
g_2.fun_2(2); g_3.fun_1("g_3_1");//jre1.7及以上
g_3.fun_2(3);
如下图:
从上图可以很明显的看到,在编译的时候,编译器就已经为范型确定了指定的类型,这个时候如果我们想要使用这些方法,就必须按照指定的类型进行传参,否则编译报错。
以此类推,g_2,g_3对象都是这样的情况。
(2)继续看一下:如果不使用范型,编译是如何让处理的呢?
//不使用范型
GenericClass g_4=new GenericClass("type_String");
如下图所示:
可以看到,在实例化范型类的时候,并没有指定类型,然后直接调用范型类中的范型方法,编译器没有找到指定的类型,就默认为Object基类来处理了!
这样的话,就会出现,传入各种引用类型均不会编译报错,但是在方法体中处理这些数据的时候,有可能出现数据类型转换失败的异常:ClassCastException。
而如果使用了范型,就可以在编译的时候抛出异常,更方便我们编码,效果会好很多。
三、范型的扩展
先补充一个知识点:
Java中数组具有“协变性”,但是集合Collection不具有协变性。
“协变性”:可以理解为兼容性。
数组的协变性的简单案例如下:
package com.xfwl.generics;
/**
* 数据结构与算法分析:第一章
* @function 数组类型的兼容性
* @author 小风微凉
* @time 2018-5-6 下午1:26:52
*/
public class ArrayParent {}
package com.xfwl.generics;
public class Test {
/**
* @param args
*/
public static void main(String[] args) {
//验证数组具有兼容性
//创建三个数组
ArrayParent[] par={};
ArraySon_1[] son1={};
ArraySon_2[] son2={}; //开始测试
fun_3(par);
fun_3(son1);
fun_3(son2);
}
private static void fun_3(ArrayParent[] arr){}
private class ArraySon_1 extends ArrayParent{}
private class ArraySon_2 extends ArrayParent{}
}
(1)带有限制的通配符
由上面可以看出数组具有协变性,导致代码得以编译,不会在编译的时候报红,因为真正地在进行类型转换是在JVM运行的时候,如果此时有问题,会产生一个运行时异常(一个ArrayStoreException)。而使用范型的全部原因就在于产生编译器错误,而不是类型不匹配的运行时异常。所以范型集合是不协变的。
所以可以存在:传入数组参数(支持协变性)
private static void fun_3(ArrayParent[] arr){}
却不能写成:
private static void fun_3(Collection<ArrayParent> arr){}
说明:如果按照上面这行代码的效果,我们调用fun_3(...)的时候就只能传参:Collection<ArrayParent>这种类型的参数,而不支持Collection<ArraySon_1>这种类型的参数
现在的问题是,范型(以及范型集合)不是协变性的(但有意义),而数组是协变性的。若无附加的语法,则用户就会避免使用集合Collection,因为失去协变性使得代码缺少灵活性。
Java5中的“通配符”来弥补这个不足,通配符用来表示参数类型的子类(或者超类)。
现在贴出一个例子:
package com.xfwl.generics; import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator; /**
* 数据结构与算法分析:第一章
* @function 范型通配符
* @author 小风微凉
* @time 2018-5-6 下午2:46:56
*/
public class Test2<T> { public static void main(String[] args) {
System.out.println("***【1】***********************");
//开始测试:范型的通配符
Collection<ArrayParent> list=new ArrayList<>();
list.add(new ArrayParent());
list.add(new ArraySon_1());
list.add(new ArraySon_2());
fun_4(list);
Collection<ArraySon_1> list2=new ArrayList<>();
list2.add(new ArraySon_1());
fun_4(list2);//编译通过,运行正常 System.out.println("***【2】***********************");
//测试?extends T
Collection<ArraySon_1> list3=new ArrayList<>();
list3.add(new ArraySon_1());
fun_5(list3);//编译通过,运行正常 System.out.println("***【3】***********************");
Collection<ArrayParent> list4=new ArrayList<>();
list4.add(new ArrayParent());
fun_6(list4);//编译通过
//fun_6(list3);//编译不通过,报错The method fun_6(Collection<? super Test2.ArraySon_2>) in the type Test2<T> is not applicable for the arguments (Collection<Test2.ArraySon_1>)
}
private static void fun_3(ArrayParent[] arr){}
@SuppressWarnings("unused")
private static void fun_4(Collection<?> arr){//?表示随便什么类型
Iterator<?> it= arr.iterator();
for(;it.hasNext();){
System.out.println(it.next().toString());
}
}
@SuppressWarnings("unused")
private static void fun_5(Collection<? extends ArrayParent> arr){//?表示ArrayParent的子类
Iterator<?> it= arr.iterator();
for(;it.hasNext();){
System.out.println(it.next().toString());
}
}
@SuppressWarnings("unused")
private static void fun_6(Collection<? super ArraySon_2> arr){//?表示ArraySon_2的超类
Iterator<?> it= arr.iterator();
for(;it.hasNext();){
System.out.println(it.next().toString());
}
}
private static class ArraySon_1 extends ArrayParent{
@Override
public String toString() {
return "ArraySon_1_toString()";
}
}
private static class ArraySon_2 extends ArrayParent{
@Override
public String toString() {
return "ArraySon_2_toString()";
}
}
}
看一下运行结果:
***【1】***********************
com.xfwl.generics.ArrayParent@626b2d4a
ArraySon_1_toString()
ArraySon_2_toString()
ArraySon_1_toString()
***【2】***********************
ArraySon_1_toString()
***【3】***********************
com.xfwl.generics.ArrayParent@5e91993f
值得说明的是:
(1)通配符指的是“?”,不信?那么如果把参数:Collection<?> arr 改为:Collection<T> arr,会出现什么情况呢?
经过我的测试如下图:
Cannot make a static reference to the non-static type T
翻译之后:“ 不能对非静态类型T进行静态引用”
那么修改一下代码:但是这就牵扯到了范型的另外一个用法:范型的static方法,如下
(2)范型的static方法
上面的代码中,我们知道,范型不能应用与一般的static方法结构的方法定义中。如果一定要使用范型的static方法,则必须按照固定的格式去定义才可以通过编译。
static方法固定格式:
访问权限修饰符 static <范型T,...> 返回类型 方法名称(范型T t){}
根据上面的格式:简单罗列几种可能出现的方法定义的样式:如下
/**
* 范型static方法
* 格式:一定要在static后面跟上<范型>才不会编译报错
*/
public static <T> void fun_7(T t) {
System.out.println("fun_7()输出:"+t);
} public static <T> T fun_8(T t) {
System.out.println("fun_8()输出:"+t);
return t;
}
所以:此时就可以改造一下上面范型类:GenericClass<T,E>中的单例模式的代码了!
private static GenericClass generic=null;
private GenericClass(T t){
this.att1=t;
}
//单例模式-懒汉式:范型不能用于static修饰的属性或方法中
public static <T,E> GenericClass getSingleInstence(T t){
if(generic==null){
generic=new GenericClass<T,E>(t);
}
return generic;
}
分析一下:
从上面代码可以看到:静态方法getSingleInstence(T t)的修饰符<T,E>有范型T和范型E两种范型,而方法体中,也使用到了范型T和范型E。
或许你会有点好奇,既然泛型类:GenericClass<T,E>中已经申明了范型E,那么方法getSingleInstence(T t)的范型修饰符中能不能不用到E,或者T呢?
带着这个疑问,继续测试一下:
从上面两个截图中就可以看出来:范型static方法,必须在static后面紧接着跟上 这个方法体和参数定义中的范型类型,缺一不可。
但是顺序是可以不固定的:如下:
public static <E,T> GenericClass getSingleInstence(T t){}
或者
public static <T,E> GenericClass getSingleInstence(T t){}
(3) 范型类型的限定
类型限界在尖括号内指定,它指参数类型必须具有的性质。
这里有一个看起来非常浮复杂的方法定义语句,但是关于范型最复杂的方法定义语句,估计也就是这个样子的了。
/**
* 求数组中的最大值
* @param arr 范型数组
* @return 返回的最大值
*/
public static <T extends Comparable<? super T>,E> T findMax(T[] arr){
int maxIndex=0;
for(int i=0;i<arr.length;i++){
if(arr[i].compareTo(arr[maxIndex])>0){
maxIndex=i;
}
}
return arr[maxIndex];
}
简单分析一下上面这个方法吧!
其实,如果经过上面的各个案例的分析说明,这个方法的定义格式就很好理解了。
(1)理解:Comparable<? super T>
含义:理解之前需要了解Compareable这个接口是怎样的:
public interface Comparable<T> {
然后Comparable<? super T>,表示Compareable这个接口的比较类型?(通配符)是一个范型类T的父类。
(2)理解:T extends Comparable<? super T>
含义:表示实现了一个类型T,该类型T是Compareable接口实现类的子类,该Compareable接口实现类的比较类型是T的父类(超类或基类)。
(好像是有点绕啊,有更好的理解,欢迎指点。)
(3)理解:<T extends Comparable<? super T>,E>
含义:声明两种范型类型
(4)理解:T[] arr
含义:传入的参数,范型T的数组
(5)理解:findMax方法体
含义:比较找出当前数组中的最大的数据。运用了CompareTo方法,需要说明的是,当前的范型T的对象,都是实现了接口Compareable的类的对象。
(4)范型的类型擦除
范型在很大程度上是Java语言中的成分而不是虚拟机中的结构,范型可以由编译器通过所谓的“类型擦除”(type erasure)过程而转变成非泛型类。这样,编译器就生产一种与范型类同名的原始类,
但是类型参数都被删除了。类型变量由他们的类型限界来代替,当一个具有擦除返回类型的范型方法被调用的时候,一些特性被自动地插入。如果使用一个范型类而不带类型参数,那么使用的是原始类。
类型擦除的一个重要的推论是,所生产的代码与程序员在范型之前所写的代码并没有太多的差异,而且事实上运行的也并不快。其显著地优点在于,程序员不比把一些类型转换放到代码中,编译器经进行重要的类型检查。
(5)对于范型的限制
对于范型有许多的限制,由于类型擦除的原因,这里列出的每一个限制都必须遵守。
- 基本类型
基本类型不能用作类型参数,因此,GenericClass<T,E>正确,但是GenericClass<int> 是非法的。必须使用包装类!
- instanceof检测
instanceof检测和类型转换只对原始类型进行
- static的语境
在一个范型类中,static方法和static域均不可引用类的类型变量,因为在类型擦除后类型变量就不存在了,另外,由于实际上只存在一个原始的类,因此static域在诸多范型实例之间是共享的。
- 范型类型的实例化
不能创建一个范型类型的实例,如果T是一个变量,则语句: T t=new T();//是非法的。 T由他的界限替代,可能是Object或者是抽象类,所以对new的调用没有意义。
- 范型的数组对象
不能创建一个范型数组,如果T是一个类型变量,则语句 T[] arr=new T[10];//是不合法的。T将由他的界限代替,可很能是Object T,于是(由于类型擦除产生的)对T[]的类型转换将无法进行,因为Object[] 不是T[]。
- 参数化类型的数组
参数化类型的数组的实例化是非法的。比如下面这样的:
GenericClass<String, Integer>[] g_1 =new GenericClass<> [10];//这是非法的,会报编译异常
好了,范型的学习也差不多了,到此告一段落了。以后有新的发现会继续研究。
Java数据结构与算法分析-第一章(引论)-Java中的范型<T,E>构件的更多相关文章
- 【学习总结】java数据结构和算法-第一章-内容介绍和授课方式
总目录链接 [学习总结]尚硅谷2019java数据结构和算法 github:javaDSA 目录 几个经典算法面试题 算法和数据结构的重要性 几个经典算法面试题 字符串匹配 暴力法:慢 kmp算法:更 ...
- 20145330《Java学习笔记》第一章课后练习8知识总结以及IDEA初次尝试
20145330<Java学习笔记>第一章课后练习8知识总结以及IDEA初次尝试 题目: 如果C:\workspace\Hello\src中有Main.java如下: package cc ...
- Java 面向对象编程——第一章 初识Java
第一章 初识Java 1. 什么是Java? Java是一种简单的.面向对象的.分布式的.解释的.安全的.可移植的.性能优异的多线程语言.它以其强安全性.平台无关性.硬件结构无关性.语言简 ...
- 编写高质量代码:改善Java程序的151个建议(第一章:JAVA开发中通用的方法和准则)
编写高质量代码:改善Java程序的151个建议(第一章:JAVA开发中通用的方法和准则) 目录 建议1: 不要在常量和变量中出现易混淆的字母 建议2: 莫让常量蜕变成变量 建议3: 三元操作符的类型务 ...
- Java 学习笔记 第一章:Java语言开发环境搭建
第一章:Java语言开发环境搭建 第二章:常量.变量和数据类型 第三章:数据类型转换.运算符和方法入门 1.Java虚拟机——JVM JVM(Java Virtual Machine ):Java虚拟 ...
- “全栈2019”Java第八十九章:接口中能定义内部类吗?
难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java第 ...
- “全栈2019”Java第八十四章:接口中嵌套接口详解
难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java第 ...
- “全栈2019”Java第七十九章:类中可以嵌套接口吗?
难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java第 ...
- 使用Micrisoft.net设计方案 第一章 企业解决方案中构建设计模式
第一章企业解决方案中构建设计模式 我们知道的系统总是由简单到复杂,而不是直接去设计一个复杂系统.如果直接去设计一个复杂系统,结果最终会导致失败.在设计系统的时候,先设计一个能够正常工作的系统,然后在此 ...
随机推荐
- CodeForces - 961D:Pair Of Lines (几何,问两条直线是否可以覆盖所有点)
You are given n points on Cartesian plane. Every point is a lattice point (i. e. both of its coordin ...
- docker容器与宿主机之间内容拷贝
来自:http://blog.csdn.net/yangzhenping/article/details/43667785 常用的方式有3种: 从容器内拷贝文件到主机上 docker cp <c ...
- java-04类和对象课堂练习
1.请运行并输入以下代码,得到什么结果 public class Test { public static void main(String[] args){ Foo obj1=new Foo(); ...
- [HihoCoder1413]Rikka with String
vjudge 题意 给你一个串,问你把每个位置的字符替换成#后串中有多少本质不同的子串. \(n\le 3*10^5\) sol 首先可以计算出原串里面有多少本质不同的子串.显然就是\(\sum_{i ...
- C++对C语言的拓展(4)—— 函数重载
函数重载(Function Overload):用同一个函数名定义不同的函数,当函数名和不同的参数搭配时函数的含义不同. 1.重载规则 (1)函数名相同: (2)参数个数不同,参数的类型不同,参数顺序 ...
- 洛谷【P1004】方格取数
浅谈\(DP\):https://www.cnblogs.com/AKMer/p/10437525.html 题目传送门:https://www.luogu.org/problemnew/show/P ...
- php用zendstudio建立wsdl
首先,新建的时候要选择soap,然后deocument和rpc都可以. 类和方法的页面: <?php //发货接口 class test{ function send_do_delivery($ ...
- Network(lca暴力)
Network Time Limit : 2000/1000ms (Java/Other) Memory Limit : 65536/65536K (Java/Other) Total Submi ...
- socket模型
Socket: "主机" + "端口" = 套接字/插座; 仅仅是一个通信模型,不属于七层协议(网络协议). 一台电脑(IP)的一个应用程序(端口) 和 另一台 ...
- Git命令之创建版本
安装 安装好Git后,将会在桌面生成 这样一个图标 运行后将会是类似控制台程序的黑色窗口,其中mingw64(参考百度百科).这样的话就可以在输入命令 例如 :git 见到下图有详细的用法表示成功否则 ...