前言
记录学习过程
泛型是一个很重要的知识点
其实用过集合就都对泛型有一定的了解
目录
泛型
泛型:把明确数据类型的工作推迟到创建对象或调用方法的时候才去明确的特殊的类型
Java泛型设计原则:只要在编译时期没有出现警告,那么运行时期就不会出现ClassCastException异常
相关术语:
- ArrayList中的E称为类型参数变量
- ArrayList中的Integer称为实际类型参数
- 整个称为ArrayList泛型类型
- 整个ArrayList称为参数化的类型ParameterizedType
泛型是在jdk1.5引进的,jdk1.5以前往列表添加数据:
List list=new ArrayList();
list.add("a");
list.add(1);
for(int i=0;i<list.size();i++){
System.out.println(list.get(i));
System.out.println((list.get(i)).getClass().toString());
}
可以看出没有限定输入数据类型,很不规范
在jdk1.5引进泛型后:
list就只能保存一个类型的数据,不然在编译时就会报错
可以明白泛型的概念:把我们想要操作的数据类型保存为参数,即 < String>
泛型的特点
(1)类型安全:我们在使用泛型之后,可以指定输入的类型,比如只能输入String类型的值,输入其他的就会报错,这在代码编写时,为我们提供了极大的方便。只要编译时期没有警告,那么运行时期就不会出现ClassCastException异常
(2)消除强制类型转换:也就是说我们不需要进行类型转化,直接存储、直接输出,代码更简洁。
(3)只在编译器有效:也就是说在运行时泛型是无效的。这避免了jvm花费时间在运行时做额外的操作。
总结就是让程序更简洁、健壮、可读性高、稳定性强
第(3)点可以通过反射验证一下:
//通过泛型、非泛型获得两个列表
List list=new ArrayList();
List<String> stringList=new ArrayList<>();
//反射得到类对象
Class listClass=list.getClass();
Class stringListClass=stringList.getClass();
System.out.println(listClass == stringListClass);
学习反射的时候知道:一个类的类对象class只有一个
可以看出泛型只在编译期有效,到了运行期泛型就无效了,和最初的列表一样
foreach遍历明显就是基于泛型才可以使用的
如果不是泛型的列表:
可以看出早期的非泛型是用Object来代表任意类型的,所以需要类型转换,即第(2)点
而泛型的列表就可以简单的使用foreach遍历:
泛型的使用
泛型的使用主要是在三个方面,泛型类、泛型接口、泛型方法
泛型类
泛型类很明显就是把泛型定义到类上
创建一个MyClass类:
package com.company.entity;
public class MyClass<T,E> {
private T classId;
private E className;
public MyClass(T classId, E className) {
this.classId = classId;
this.className = className;
}
public T getClassId() {
return classId;
}
public void setClassId(T classId) {
this.classId = classId;
}
public E getClassName() {
return className;
}
public void setClassName(E className) {
this.className = className;
}
@Override
public String toString() {
return "MyClass{" +
"classId=" + classId +
", className=" + className +
'}';
}
}
可以看出MyClass类没有定义数据类型,这样就可以很方便的使用
创建一个测试类:
package com.company.javaBasis;
import com.company.entity.MyClass;
public class TestMyClass {
public static void main(String[] args){
MyClass<Integer,String> myClass=new MyClass<>(1,"Java");
System.out.println(myClass.toString());
MyClass<String,Integer> myClass1=new MyClass<>("JavaScript",2);
System.out.println(myClass1.toString());
}
}
可以看出使用了泛型后,根据用户的想法,想怎么改类型就怎么改
需要注意:
(1)实例化泛型类时,必须指定E和T的具体类型,比如这里指定的是Integer和String
(2)指定的具体类型必须是类,不能是int,float等这些基础类型
(3)不能对泛型类使用instanceof。为什么呢?这是因为泛型类只在编译期有效,在运行时期不区分是什么类型。
例:
泛型接口
泛型定义到接口上就是泛型接口
泛型接口:
package com.company.javaBasis;
public interface SayHello<T> {
//这是一个普通的方法,不是泛型方法
public void say(T t);
}
测试类:
package com.company.javaBasis;
public class TestSayHello implements SayHello<String>{
@Override
public void say(String string) {
System.out.println("hello,"+string);
}
public static void main(String[] args){
TestSayHello testSayHello=new TestSayHello();
testSayHello.say("zhangsan");
}
}
需要注意:
(1)继承泛型接口的时候就需要指定具体是什么类型
(2)泛型中的方法也需要对相应的泛型参数赋予具体的类型。
泛型方法
只在某个方法上使用泛型,即我们只关心这个方法,对他的类不做要求
泛型方法:
package com.company.javaBasis;
public class TestSayHello implements SayHello<String>{
//带返回值泛型方法
public <T> T sayBye(T t){
return t;
}
//无返回值泛型方法
public <T> void sayBye1(T t){
System.out.println("Bye,"+t);
}
@Override
public void say(String string) {
System.out.println("hello,"+string);
}
public static void main(String[] args){
TestSayHello testSayHello=new TestSayHello();
testSayHello.say("zhangsan");
//测试泛型方法
String str=testSayHello.sayBye("Bye");
System.out.println(str);
testSayHello.sayBye1("zhangsan");
}
}
泛型标志< T >在权限修饰符后
继承泛型类
泛型类是拥有泛型这个特性的类,它本质上还是一个Java类,那么它就可以被继承
所以继承泛型类就可以分成两种:
-
子类明确泛型类的类型参数变量
-
子类不明确泛型类的类型参数变量
也就是在子类选择要不要明确数据类型
子类明确泛型类的类型参数变量
刚才的泛型接口就是子类明确泛型类的类型参数变量
在继承implements里已经确定了类型参数变量< String >
子类不明确泛型类的类型参数变量
编写一个子类继承SayHello接口,并且不明确类型参数变量
package com.company.javaBasis;
public class TestSayHelloNoSure<T> implements SayHello<T> {
@Override
public void say(T t) {
System.out.println("Say "+t);
}
public static void main(String[] args){
TestSayHelloNoSure<String> testSayHelloNoSure=new TestSayHelloNoSure<>();
testSayHelloNoSure.say("hello");
}
}
可以看出继承泛型类和继承普通类一样,,只有看用户要不要确定类型参数变量< T >
通配符
当我们不清楚泛型究竟是什么类型,但又必须要用时
例如一个类,输入的参数是List类型
没有学泛型:
public void MyList(List list){
for(int i=0;i<list.size();i++){
System.out.println(list.get(i));
}
}
这种写法在编译的时候会被警告没有确定集合类型
学习了泛型,就可以用通配符来表示我们不确定的集合类型:
public void MyList(List<?> list){
for(int i=0;i<list.size();i++){
System.out.println(list.get(i));
}
}
?号通配符表示可以匹配任意类型
注意:只能调用与对象无关的方法,不能调用对象与类型有关的方法,即别在方法内使用add()之类的方法,因为不确定List的类型
而没有通配符:
居然是可以加入属性的,这样就可以会报错
可以看出使用通配符?,可以使代码更加安全,优雅
通配符相关
泛型中有三种通配符形式:
(1) 无限制通配符:表示我们可以传入任意类型的参数
(2) 表示类型的上界是E,只能是E或者是E的子孙类。
(3) 声明了类型的下界E,只能是E或者是E的父类。
无限制通配符
就像我们前面使用的List< ?> list,这就没有限制
通配符上界
写法:< ? extends Type>
例:
List<? extends Number> list
这个语句的意思是list只能传入Number类或子类
当加上通配符上界:
public void MyList(List<? extends Number> list){
for(int i=0;i<list.size();i++){
System.out.println(list.get(i));
}
}
可以看到,想要往里面加String类型的数据集合就编译报错
通配符下界
写法: <? super Type>
例:
List<? super Number> list
意思是只能装入Number类或它的父类
和上界 extends类似,都是限制传入类型
通配符和泛型方法的选择
学习了通配符,发现它和泛型方法有些类似
前面的泛型方法:
public <T> void test2(List<T> t) {
}
通配符:
public static void test(List<?> list) {
}
如何选择?
原则:
- 如果参数之间的类型有依赖关系,或者返回值是与参数之间有依赖关系的。那么就使用泛型方法
- 如果没有依赖关系的,就使用通配符,通配符会灵活一些.
把一个对象分为声明、使用两部分的话
泛型侧重于类型的声明上代码复用,通配符则侧重于使用上的代码复用。
泛型用于定义内部数据类型的不确定性,通配符则用于定义使用的对象类型不确定性
类型擦除
前面有反射得出了:泛型只在编译期有效,在运行期虚拟机是分辨不出来的
也就是在编译期Java编译期将泛型擦除了
创建两个同名参数类型不同的方法:
public void MyList(List<String> list){
for(int i=0;i<list.size();i++){
System.out.println(list.get(i));
}
}
public void MyList(List<Integer> list){
for(int i=0;i<list.size();i++){
System.out.println(list.get(i));
}
}
对泛型擦除之后,这两个同名方法没有不同,所以就报错了
那< T>类型擦除后会是什么?
通过反射试试:
获得泛型接口的类对象:
package com.company.javaBasis;
import java.lang.reflect.Method;
public class TestGeneric {
public static void main(String[] args){
try {
Class myclass=Class.forName("com.company.javaBasis.SayHello");
Method[] methods=myclass.getMethods();
for(Method method:methods)
System.out.println(method.toString());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
是Object对象,这就是最初没有泛型时,都是使用Object类型表示所有类型
之前的类型擦除,都是直接擦除到Object,如何限制擦除到一定程度?
使用边界,让编译期擦除到边界就不擦除了
interface A{
void testA();
}
interface B{
void testB();
}
public class TestGeneric {
public static class Test<T extends A & B>{
private T val;
public Test(T val){
val = val;
}
public void test(){
val.testA();
val.testB();
}
}
}
编译成功,因为<T extends A & B>声明了Test必须具有类型A与B
所以就可以安全的使用A的testA方法和B的testB方法,不必担心类型擦除后没用
public static class Test<T>{
private T val;
public Test(T val){
val = val;
}
public void test(){
val.testA();
val.testB();
}
}
当然,编译期会报错
类型擦除如果指定了类型参数的上界的话,则使用这个上界
总结
泛型是一个语法糖,语法糖就是一个方便程序员的功能,对语言没有任何影响
使用泛型可以提升代码的复用
学习过程跟随大佬的脚步
泛型就是这么简单
深入理解Java中的泛型
类型擦除详解