Java泛型
1.概述
泛型:把类型明确的工作推迟到创建对象或调用方法的时候才去明确的特殊的类型。也就是说在创建对象或调用方法时才知道具体的类型,而在定义类或方法时不需要明确,而是使用通用的类型代替。在使用时把类型当做参数进行传递。
设计原则:只要在编译时期没有出现警告,那么运行时期就不会出现ClassCastException异常。
泛型的由来:早期Java是使用Object来代表任意类型的,但是向下转型有强转的问题,这样程序就不太安全。而使用泛型,不用强制转换使得代码更加简洁,提升了可读性和稳定性。
但是泛型只存在于代码编辑阶段,在进入JVM前,与泛型相关的信息都会被擦除掉,泛型类型会变成Object类型,这个现象叫做类型擦除。
2.泛型常用值
泛型有几种的常用的值,如下表
值 | 说明 |
T | Type的简称,代表java类 |
E | Element的简称,代表元素 (用在集合中) |
K | Key的简称,代表键,主要用于Map类 |
V | Value的简称,代表值,主要用于Map类 |
? | 类型通配符,表示不确定的类型,可使用任意类型 |
3. T的用法
3.1泛型类
把泛型定义在类上,使用该类的时候,才把类型明确下来。然后在方法中就可以使用传入的类型。
1)定义泛型类
需求:传入一个List集合,无论什么类型,只要有元素就取第一条数据。
import java.util.List; public class MyClass<T> { // 1 public T getFirst(List<T> list) { //2 if (list == null || list.size() == 0) { return null; } return list.get(0); } }
在代码中,1处的<T>是给类添加了泛型。2处有两个T,前面的T是指定方法的返回值。后面的<T>是给List类添加了泛型,从而保证List的元素类型和方法返回值一致。
2)使用泛型类
public static void main(String[] args) { MyClass<String> myClass = new MyClass<>(); List<String> list = new ArrayList<>(); list.add("张三"); list.add("tom"); System.out.println(myClass.getFirst(list));//张三 MyClass<Map<String, String>> myClass2 = new MyClass<>(); List<Map<String, String>> list2 = new ArrayList<>(); Map<String, String> map = new HashMap<>(); map.put("name", "tom"); Map<String, String> map2 = new HashMap<>(); map2.put("age", "20"); list2.add(map); list2.add(map2); System.out.println(myClass2.getFirst(list2));//{name=tom} //判断类的类型是否一致 System.out.println(myClass.getClass() == myClass2.getClass());//true }
当传入String类型时,就返回String类型;传入Map类型时就返回Map类型。如果不传入类型,则按Object类型来操作。而且最后一行在判断使用同一泛型类创建不同类型的对象,本质上是同一类型,这个类型就是泛型类。
需要注意的是,泛型类不支持基本数据类型,只支持引用数据类型。
另外,关于泛型类的派生子类,也有一定的要求。当派生子类是泛型类时,则其泛型必须和父类的泛型保持一致;当派生子类不是泛型类时,则必须明确父类的数据类型。
下面以两个案例进行说明:父类以MyFirst为例
①派生子类是泛型类
public class MyChild1<T> extends MyClass<T> { @Override public T getFirst(List<T> list) { return super.getFirst(list); } }
②派生子类不是泛型类
public class MyChild2 extends MyClass<String> { @Override public String getFirst(List<String> list) { return super.getFirst(list); } }
这里必须明确类型,否则编辑器就验证不通过。
泛型接口也是泛型类的一种,只不过它是接口。两者使用极为相似,不同之处在于子类对接口是实现,而子类对普通类是继承。那么对于泛型接口,当实现类类是泛型类时,则其泛型必须和接口的泛型保持一致;当实现类不是泛型类时,则必须明确接口的数据类型。
3.2泛型方法
若仅仅在某一个方法上需要使用泛型,那么就不需要对整个类添加泛型,而是给方法添加泛型即可。因为泛型方法的类型是在调用时才确定的,只与传入的类型有关。
将上述的泛型类进行修改,变成泛型方法:
1)定义泛型方法
import java.util.List; public class MyClass { public <T> T getFirst(List<T> list) { if (list == null || list.size() == 0) { return null; } return list.get(0); } }
在方法上有三个T,第一个<T>是指定此方法是一个泛型方法,第二个和第三个T的用法同上。
2)使用泛型方法
public static void main(String[] args) { MyClass myClass = new MyClass(); List<String> list = new ArrayList<>(); list.add("张三"); list.add("tom"); System.out.println(myClass.getFirst(list));//张三 MyClass myClass2 = new MyClass(); List<Map<String, String>> list2 = new ArrayList<>(); Map<String, String> map = new HashMap<>(); map.put("name", "tom"); Map<String, String> map2 = new HashMap<>(); map2.put("age", "20"); list2.add(map); list2.add(map2); System.out.println(myClass2.getFirst(list2));//{name=tom} }
通过对比可以发现,这种方式比泛型类简单一点,但效果是一样的,可根据需求进行选择。上述的泛型方法在创建对象后才能使用,当然也可以创建静态的泛型方法。也就是说泛型方法与是否是静态的无关。
另外,泛型方法也支持可变参数,也就是在泛型的后面使用...即可:
public class TestUtil { //随机获取集合中的某个元素 public static <T> T getValue(List<T> list) { Random random = new Random(); return list.get(random.nextInt(list.size())); } //根据传入的参数创建集合 public static <E> List<E> createList(E... t) { ArrayList<E> list = new ArrayList<>(); for (E e : t) { list.add(e); } return list; } }
调用方法
public static void main(String[] args) { List<Integer> list = new ArrayList<>(Arrays.asList(1, 3, 5, 9, 3)); System.out.println(TestUtil.getValue(list)); System.out.println(TestUtil.createList("tom", "jim", "siri")); }
4. E的用法
由于E表示元素,主要用在List集合,因此直接放List的部分源码:
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { private static final long serialVersionUID = 8683452581122892189L; .... }
这是ArrayList类的定义,使用E作为泛型。
5. K、V的用法
由于K、V表示键和值,主要用在Map中,因此直接放Map的部分源码:
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { private static final long serialVersionUID = 362498820763181265L; ... }
6. ?的用法
?表示类型通配符,表示任何类型,但在单独使用时只能用在方法的参数类型上,因为它是类型实参,不是类型形参。一般可用在没有返回值的场景中。
6.1定义泛型方法
需求:遍历不同类型的集合的元素并打印
public void listFor(List<?> list){ if (list != null && list.size() != 0) { list.forEach(item->{ System.out.println(item); }); } }
这里使用?代替了集合的元素类型。
6.2 Class<?>用法
上一小节,传递的参数都是先创建了对象并赋值,再把这个对象通过参数进行传递的。那么当只需要明确参数的类型,不需要对象中的值时,可以使用Class<?>。可以用在类的类型和方法的参数上。
1)用在方法的参数上
public void getClassField(Class<?> pojoClass) throws NoSuchFieldException { Field field = pojoClass.getClass().getDeclaredField("name"); System.out.println(field.getGenericType()); }
上述代码是通过反射机制用来获取类中name属性的类型,若不存在就会抛出异常。
调用方式:
MyClass myClass2 = new MyClass(); myClass2.getClassField(MyClass2.class);
传递时,指定的类,后面使用.class标识。除此之外,还可以用在构造方法上,例如:
public class MyClass2 { private Class<?> entityClass; public MyClass2(Class<?> entityClass) { this.entityClass = entityClass; } }
调用:
MyClass2 myClass22 = new MyClass2(String.class);
在创建对象时传入指定类型。
6.3 <? extends T>与 <? super T>的用法
这两者都是限定通配符。T代表实际传入的类型
<? extends T>来确保传入的类型必须是T的子类来设定类型的上限,get优先,put受限,适用于提取元素为主的场景。
<? superT>来确保传入的类型必须是T的父类来设定类型的下限,put优先,get受限,适用于存放元素为主的场景。
7.泛型的类型擦除
泛型只在代码编译时存在,进入JVM前数据类型会变成Object类型。
1)无限制类型擦除
擦除图示
代码演示:利用反射获取字节码文件
public static void main(String[] args) { Student<String> stu = new Student<>(); Class<? extends Student> aClass = stu.getClass(); //获取所有的字段信息 Field[] declaredFields = aClass.getDeclaredFields(); Arrays.stream(declaredFields).forEach(System.out::println); //获取所有的方法 Method[] methods = aClass.getDeclaredMethods(); Arrays.stream(methods).forEach(System.out::println); }
打印结果
可以看出变量类型和方法的类型全部变成了Object类型。
2)有限制类型擦除
限制了类型的上限
代码演示
3)桥接方法
对于泛型接口,当实现类不是泛型类时,在编译时会生成一个桥接方法,保持接口和类的实现关系
泛型接口
public interface Info<T> { T getInfo(T t); }
实现类
public class InfoImpl implements Info<String>{ @Override public String getInfo(String s) { return s; } }
代码实现
是不是很奇怪,明明实现类只有一个方法,但这里却获取到了两个同名的方法,只是数据类型不同。Objetc对应的这个方法就是接口的桥接方法,因此此时接口中方法的数据类型是Object,为了和接口保持一致,故会在实现类生成一个方法与接口保持实现关系。