本文整理自B站:JavaSE强化教程泛型,由点到面的讲解了整个泛型体系
- 第一章:泛型概述
- 一:泛型概念
- 二:泛型类
- 1:泛型类使用
- 2:泛型类派生子类
- 1):子类也是泛型类
- 2):子类不是泛型类
- 三:泛型接口
- 1:实现类不是泛型类
- 2:实现类也是泛型类
- 四:泛型方法
- 1:概念
- 2:具体使用
- 3:静态泛型方法
- 4:泛型方法与可变参数
- 第二章:泛型通配符
- 一:什么是类型通配符
- 二:类型通配符上限
- 三:类型通配符下限
- 第三章:类型擦除
- 一:无限制类型擦除
- 二:有限制类型擦除
- 三:擦除方法中类型参数
- 四:桥接方法
- 第四章:泛型与数组
- 一:带泛型数组的对象不可以直接创建
- 二:通过反射创建泛型的数组
- 第五章:泛型与反射
- 一:反射中常用的泛型类
- 二:泛型对反射的好处
第一章:泛型概述
一:泛型概念
JAVA推出泛型以前,程序员可以构建一个元素类型为Object的集合,该集合能够存储任意的数据类型对象,而在使用该集合的过程中,需要程序员明确知道存储每个元素的数据类型,否则很容易引发ClassCastException异常。
Java泛型(generics)是JDK5中引入的一个新特性,泛型提供了编译时类型安全监测机制,该机制允许我们在编译时检测到非法的类型数据结构。
泛型的本质就是参数化类型,也就是所操作的数据类型被指定为一个参数。
二:泛型类
泛型标识可以理解称为一个类型的形参,我们拿着这个泛型标识可以去声明泛型变量+方法的返回值类型和参数类型。
常用的泛型标识T、E、K、V其中KV都是结合使用。
1:泛型类使用
package com.dashu.nettytest;
public class Generic<T> {
private T t;
public Generic(T t){
this.t = t;
}
public T getT(){
return t;
}
public void setT(T t){
this.t = t;
}
}
这里的泛型就是一个泛型标识,它做了成员变量的类型,普通方法的入参和出参,构造方法的入参。
我们在创建对象的时候来指定T的具体类型,假如我们传入的是String类型,那么就说明我们要在这个类里边玩String类型了。
这里T是有外部使用类的时候传入的。
创建泛型类对象时如果并没有指定泛型,那么这里边的泛型将会按照Object类型来进行操作。
泛型类不支持基本数据类型,仅仅支持引用类型。我们真正使用T的时候,是转换成了Object,在使用类里边的泛型成员的时候,会在一个适当的机会将Object传递过来的数据类型转换为适当的类型,而我们的基本数据类型并不是继承自Object,玩具发进行转换。
使用泛型之后的对象的Class对象也都是一个。他们的Class对象的地址值也是一个。
2:泛型类派生子类
1):子类也是泛型类
这里我们定义的父类,我们使用E作为泛型标志。
package com.dashu.nettytest;
public class Parent<E> {
private E value ;
public E getValue(){
return this.value;
}
public void setValue(E e){
this.value = e;
}
}
父类是泛型类,子类也是泛型类的这种情况,子类和父类的泛型标志要一样来标识子类和父类是同一个泛型。
假如我们在这里不给父类声明泛型变量的话:
public class Son<T> extends Parent {
public Object getValue(){
return super.getValue();
}
}
不给父类声明泛型变量,那么他默认就是Object那么我们使用开发工具将父类的getValue()方法重写之后就返回的是Object类型的变量。
假如我们给父类声明变量但是父子泛型变量不一致的话:
public class Son<T> extends Parent<E> {
public E getValue(){
return super.getValue();
}
}
就会发生下面的问题:
所以,子类继承泛型父类的时候,父子类要是都使用泛型功能的的话,标志必须要一致。这样做的话也是好理解的,我们创建子类对象的时候,必须先去创建父类对象,创建父类对象的话就会把泛型标识传递给父类。当然,子类泛型和父类泛型一致即可,我们也可以在子类的泛型当中进行扩展,只要保证父类的泛型标志在子类当中都有即可。
2):子类不是泛型类
子类不是泛型类,父类必须明确泛型的数据类型
public class Son extends Parent<Integer>{
}
我们如果定义的时候这样定义就会报错:
编译报错原因就是必须要指定具体的引用类型,而且这里也明确提示了,请创建E这个类,说明编译器在这里也认为E是一个具体类型,但是没有被创建而已。
三:泛型接口
实现类不是泛型类,接口要明确数据类型
实现类也是泛型类,实现类和接口的泛型类型要一致。
声明泛型接口和普通接口是一样的,声明泛型接口,添加的泛型可以作为方法的入参和返回值。
1:实现类不是泛型类
package com.dashu.nettytest;
public interface Generator<T> {
T getKey();
}
class Apple implements Generator {
@Override
public Object getKey() {
return null;
}
}
class Banana implements Generator<String> {
@Override
// public Object getKey() {
//指定了泛型类型为String类型之后,这里不能返回Object了,重写返回类型要一致。
public String getKey() {
return null;
}
}
子类不是一个泛型类,必须指明接口泛型,如果不指明的话默认就是Object。
2:实现类也是泛型类
子类是泛型类的话,父类如果也要使用泛型类的话,父子类型需要一致。当然子类可以拓展,但是必须包含父类的泛型标志。
四:泛型方法
泛型类是在实例化类的时候,指明泛型的具体类型
泛型方法是在调用方法的时候指明泛型的具体类型。
1:概念
我们上述案例中的方法不是泛型方法,只不过是一个普通的成员方法,只不过类型使用了泛型类的类型。
修饰符后边和返参结果之前的那个区域叫做泛型列表,只有声明了这个区域的方法才叫泛型方法。
方形类中使用了泛型的成员方法并不是泛型方法。
表明该方法将使用泛型类型T,此时才可以在方法汇总使用泛型类型T
T可以为任意标识,常见的就是T、E、K、V
2:具体使用
class Test{
public <B> B getArrayList(ArrayList<B> shift){
return shift.get(0);
}
}
class Test{
public <B,N,M> B getArrayList(ArrayList<B> shift){
return shift.get(0);
}
}
上述这两种写法都是没有问题的。泛型方法的泛型类是在方法被调用的时候被指定的。
3:静态泛型方法
下面是之前我一直不会的泛型方法和静态修饰符连用:
class Test{
public static final <B,N,M> B getArrayList(ArrayList<B> shift){
return shift.get(0);
}
public static void main(String[] args) {
// ArrayList<Object> list = new ArrayList();
// Object arrayList = getArrayList(list);
ArrayList<String> list = new ArrayList();
String arrayList = getArrayList(list);
}
}
这里我们入参当中反省是Object方法执行后返参就是Object,String的话返参就是String
4:泛型方法与可变参数
class Shit{
public static final <B> B getArrayList(B...b){
for (B b1 : b) {
System.out.println(b);
}
return null;
}
public static void main(String[] args) {
ArrayList<String> list = new ArrayList();
String arrayList = getArrayList("a","b","c");
Integer shit = getArrayList(1,2,3);
}
}
总结:
1:泛型方法可以独立于泛型类使用泛型的能力
2:如果static方法需要使用泛型鞥哪里,就必须成为泛型方法,不能简单使用泛型类的泛型变变量。
第二章:泛型通配符
一:什么是类型通配符
类型通配符就是一个?代替具体的类型实参,类型通配符是类型实参,而不是类型形参。
二:类型通配符上限
class Box<E>{
private E first;
public E getFirst(){
return this.first;
}
public void setFirst(E first){
this.first = first;
}
}
class Test {
public static void main(String[] args) {
Box<Integer> box = new Box<>();
showBox(box);
}
public static void showBox(Box<? extends Number> box){
Number first = box.getFirst();
System.out.println(first);
}
}
类/接口 <? extends 实参类型> 要求该泛型的类型,只能是是参类型或者是实参类型的子类类型。
这里要注意的是,类型通配符上限的这样的list是不能填充元素的。因为我们采用的是上线通配符。不知道里边的类型有多下限,所以限制填充元素。
三:类型通配符下限
类/接口<? super 实参类型> 要求该泛型的类型,只能是实参类型或者是实参类型的父类类型。
类型通配符下限这种方式,可以填充元素,但是不能保证元素的类型的 约束要求,因为可以把实参类型的子类给添加进去。
集合遍历的时候,都会使用Object来接收。
第三章:类型擦除
泛型是Java 1.5版本才引进的概念,在这之前是没有泛型的,但是,泛型代码能够很好地和之前版本的代码兼容。那是因为,泛型信息只存在于代码编译阶段,在进入JVM之前,与泛型相关的信息会被擦除掉,我们称之为–类型擦除
Java中的泛型信息,只存在于编译阶段,进入到JVM之后就没有所谓的泛型类型了。
一:无限制类型擦除
泛型标识是T,生成字节码文件过程中T都用Object替换了。我们通过反射拿对应成员变量的类型的时候,就变成了Object。
二:有限制类型擦除
有上限类型,就把T转换成了上限类型,我们通过反射拿对应成员变量的类型的时候,就变成了Number
三:擦除方法中类型参数
这里T变Number,反射去拿就行。
四:桥接方法
生成了一个桥接方法,来保证实现关系。
package com.dashu.nettytest;
import java.lang.reflect.Method;
public interface Apple <T>{
T info (T t);
}
class infoImpl implements Apple<Integer>{
@Override
public Integer info(Integer integer) {
return integer;
}
}
class Test999{
public static void main(String[] args) {
Class<infoImpl> infoClass = infoImpl.class;
Method[] declaredMethods = infoClass.getDeclaredMethods();
for (Method declaredMethod : declaredMethods) {
System.out.println(declaredMethod.getName()+":"+declaredMethod.getReturnType().getSimpleName());
}
}
//info:Integer
//info:Object
//
//Process finished with exit code 0
}
有两个info方法,保证泛型,还有一个桥接方法。字节码反编译之后,没有这个了。
第四章:泛型与数组
泛型数组的创建
可以声明带泛型的数组的引用,但是不能直接创建带泛型的数组的对象。
可以通过java.lang.relect.Array的newInstance(Class,int)来创建T[]数组
一:带泛型数组的对象不可以直接创建
如图所示,直接创建的
话是无法编译过去的。我们可以声明左边这样的引用。
非泛型的可以直接创建这是没有任何问题的。
class Test999{
public static void main(String[] args) {
ArrayList[] list = new ArrayList[5];
ArrayList<String>[] listArr = list;
ArrayList<Integer> intList = new ArrayList<>();
intList.add(100);
list[0] = intList;
String s = listArr[0].get(0);
System.out.println(s);
//Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
// at com.dashu.nettytest.Test999.main(Apple.java:28)
//
//Process finished with exit code 1
}
}
这种方式使用数组是不安全的。我们声明的是泛型的ArrayList数组,指向了一个原生类型的ArrayList的数组对象,原生类型的话,可以添加任何类型的元素,导致后续类型转换报错。我们应该这么写:
class Test999{
public static void main(String[] args) {
// ArrayList[] list = new ArrayList[5];
// ArrayList<String>[] listArr = list;
ArrayList<String>[] listArr = new ArrayList[5];
ArrayList<String> stringList = new ArrayList<>();
stringList.add("abc");
// list[0] = intList;
listArr[0] = stringList;
String s = listArr[0].get(0);
System.out.println(s);
//abc
}
}
我们使用泛型数组的时候,我们都是拿着泛型类型去创建一个这样的引用,而创建的对象呢不能创建泛型的数据类型,而是创建原生的数据类型赋值给引用。操作的时候,我们拿着引用去操作即可。
泛型在编译器会进行泛型擦除,而数组会在整个编译器期间持有他的数据类型,他们在设计上就是有冲突的,所以不能直接创建带有泛型类型的数组对象。
我们可以通过反射的方式创建一个泛型的数组。
二:通过反射创建泛型的数组
这个是不允许的,类型都没搞清楚,就直接new对象,肯定不行。
应该这么来写:
class Test999{
public static void main(String[] args) {
Fruit<String> fruit = new Fruit<>(String.class,3);
fruit.put(0,"aaa");
fruit.put(1,"bbb");
fruit.put(2,"ccc");
System.out.println("Arrays.toString(fruit.getArray()) = " + Arrays.toString(fruit.getArray()));
}
//Arrays.toString(fruit.getArray()) = [aaa, bbb, ccc]
}
class Fruit<T> {
private T[] array;
public Fruit(Class<T> clz, int length){
array =(T[]) Array.newInstance(clz,length);
}
public void put(int index,T t){
array[index] = t;
}
public T get(int index){
return array[index];
}
public T[] getArray(){
return array;
}
}
最好不用泛型数组,要用泛型集合代替。
第五章:泛型与反射
一:反射中常用的泛型类
Class
Constructor
二:泛型对反射的好处
class Test999{
public static void main(String[] args) throws Exception {
Class<Person> personClass = Person.class;
Constructor<Person> constructor = personClass.getConstructor();
Person person = constructor.newInstance();
}
}
class Person {
private String name;
public String getName(){
return name;
}
public void setName(String name){
this.name = name;
}
}