Java学习之==>泛型
一、什么是泛型
泛型,即“参数化类型”,在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
二、Java中为何要引入泛型
因为继承和多态的出现,在操作一些容器类时,需要大量的对象类型判断。先来看看下面这两段代码:
public class User { private Integer id; private String name; private Integer age; private User() { return; } private User(String name) { this(); this.name = name; } private User(Integer id, String name) { this(name); this.id = id; } private User(Integer id, String name, Integer age) { this(id,name); this.age = age; } public static User of() { return new User(); } public static User of(String name) { return new User(name); } public static User of(Integer id, String name) { return new User(id, name); } public static User of(Integer id, String name, Integer age) { return new User(id, name, age); } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } @Override public String toString() { return "User{" + "id=" + id + ", name='" + name + '\'' + ", age=" + age + '}'; } }
public class Demo01 { public static void main(String[] args) { List list = new ArrayList(); // 插入User对象 list.add(User.of(1,"大叔",40)); list.add(User.of(1,"木木",30)); // 插入其他类型的对象 list.add(123); list.add("abb"); printUser(list); } public static void printUser(List list) { for (int i = 0; i < list.size(); i++) { User user = (User) list.get(i); System.out.println(user); } } }
printUser() 方法目的是遍历打印User对象,但在 main 方法中除了可以插入 User 对象,也可以插入其他类型的对象,执行的时候必然会报错。然后我们就得在代码中加入如下判断:
public static void printUser(List list) { for (int i = 0; i < list.size(); i++) { Object obj = list.get(i); if (obj instanceof User) { User user = (User) obj; System.out.println(user); } } }
这样写代码的话就非常麻烦,代码中需要添加很多判断,使用起来非常不方便,这样泛型就应运而生了,我们只需要把代码改成如下这种形式:
public class Demo01 { public static void main(String[] args) { // 这里使用泛型的作用是:list里面只能装User对象 List<User> list = new ArrayList<>(); // 插入User对象 list.add(User.of(1,"大叔",40)); list.add(User.of(1,"木木",30)); // 如果上面List不使用泛型,这里编译时不会报错,运行时才会报错 // list.add("Hello"); // list.add("World"); printUser(list); } public static<T> void printUser(List<T> list) { for (int i = 0; i < list.size(); i++) { Object obj = list.get(i); System.out.println(obj); } } }
从以上代码可以看出,有了泛型以后,我们在代码中只需要在定义容器时给它指定一种类型,那么这个容器就只能存放该类型的对象,在业务代码中就不再需要对对象的类型进行判断,简化了很多代码的编写。泛型还可以认为是一种约定,为了使用方便,约定一个容器中只能存放某一种类型的对象。
三、泛型的使用
泛型有三种使用方式,分别是:泛型类、泛型接口和泛型方法。
1、泛型类
泛型类用于类的定义当中,通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种容器类,如:List、Set、Map。
/** * 此处 K,V 可以随便写为任意标识,常见的如K、V、T、E等形式的参数常用于表示泛型 * 在实例化泛型类时,必须指定T的具体类型,如:String、Integer等。 */ public class Generic<K, V> { /** * key,val这个成员变量的类型分别为 K,V 这两个类型由外部指定 */ private K key; private V val; /** * 泛型类的构造方法的形参 k和v 的类型也为 K,V,同样由外部指定 */ public Generic(K k, V v) { this.key = k; this.val = v; } /** * 泛型中普通方法的返回值类型 K,V 同样由外部指定 * 注意:以下这种不是泛型方法 */ public K getKey() { return key; } public V getVal() { return val; } }
但是,在使用泛型类时就一定要传入类型实参吗?在语法上是不一定的,可以不传,但是使用时最好按照约定传递,否则我们定义泛型类就没有意义了。如果不传入类型实参的话,就可以往容器内添加任何类型的对象,这样还说得在业务代码种进行类型判断。
泛型类的使用还有一种方式,使用 extends 和 super 关键字来限制我们使用传入参数的类型,如下:
/** * 此处 V extends Person 限制了 V 的类型只能使用 Person和它的子类 */ public class Generic<K, V extends Person> { private K key; private V val; public Generic(K k, V v) { this.key = k; this.val = v; } public K getKey() { return key; } public V getVal() { return val; } }
2、泛型接口
泛型接口与泛型类的定义及使用基本相同。泛型接口常被用在各种类的生产器中,可以看一个例子:
public interface Generic<K, V> { public K test01(); public V test02(); }
当实现泛型接口的类,未传入泛型实参时:
/** * 未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中 * 即:public class testGeneric<K, V> implements Generic<K, V> * 如果不声明泛型,如:public class testGeneric implements Generic,编译器会报错 */ public class testGeneric<K, V> implements Generic<K, V> { @Override public K test01() { return null; } @Override public V test02() { return null; } }
当实现泛型接口的类,传入泛型实参时:
/** * 在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型 */ public class testGeneric<String, Integer> implements Generic<String, Integer> { @Override public String test01() { return null; } @Override public Integer test02() { return null; } }
3、泛型方法
在java中,泛型类的定义非常简单,但是泛型方法就比较复杂了。
泛型类,是在实例化类的时候指明泛型的具体类型。泛型方法,是在调用方法的时候指明泛型的具体类型 。
public class Generic<K, V> { private K k; private V v; /** * 泛型方法 * public 与 返回值中间的 <T> 非常重要,可以理解为声明此方法为泛型方法。 * <T>表明该方法将使用泛型类型 T,此时才可以在方法中使用泛型类型 T。 * 与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型 * 此处的 T 与上面泛型类定义的 K 和 V 没有任何关系,可以一样,也可以不一样 * 泛型方法可以在泛型类种定义,也可以在普通类当中定义 */ public <T> T getType(T type){ return type; } /** * 静态泛型方法 */ public static <S, T> Generic<S, T> of() { /** * 类型推断 * return new Generic<S, T>() -> return Generic Pair<>() */ return new Generic<>(); } /** * 返回值或者参数带有泛型的不是泛型方法 */ public K getK() { return k; } public V getV() { return v; } }
泛型方法能使方法独立于类而产生变化,如果能做到,你就该尽量使用泛型方法。
泛型方法中同样支持使用 extends 和 super 关键字来限制我们使用传入参数的类型,如下:
public class testGeneric { public static void main(String[] args) { Person person = null; Str str = null; User user = null; getType(person); getType(str); // User不是Person或其子类,所以会报错 getType(user); } /** * 泛型方法 */ public static <T extends Person> T getType(T type){ return type; } }
User不是Person或其子类,所以会报错。Str是Person的子类,所以不会报错。
4、泛型通配符
泛型通配符只能作为方法的形参使用,如下:
public class App { public static void main(String[] args) { List<String> list1 = new ArrayList<>(); list1.add("hello"); list1.add("world"); List<Integer> list2 = new ArrayList<>(); list2.add(1); list2.add(2); print(list1); print(list2); } public static void print(List<?> list) { for (Object obj : list) { System.out.println(obj); } } }
?代表可以接收任何类型。通配符同样可以通过 extends 和 super 关键字来限制接收的类型
public class App { public static void main(String[] args) { List<Person> list1 = new ArrayList<>(); list1.add(new Person()); list1.add(new Person()); List<Str> list2 = new ArrayList<>(); list2.add(new Str()); list2.add(new Str()); List<User> list3 = new ArrayList<>(); list3.add(new User()); list3.add(new User()); print(list1); print(list2); print(list3); // 报错 } public static void print(List<? extends Person> list) { for (Object obj : list) { System.out.println(obj); } } }
四、类型擦除
泛型是 Java 1.5 版本才引进的概念,在这之前没有泛型的概念,但泛型代码能够很好地和之前版本的代码很好地兼容,是因为:泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。通俗地讲,泛型类和普通类在 Java 虚拟机内是没有什么特别的地方。我们来看看下面这段代码:
public class App { public static void main(String[] args) { List<Person> list1 = new ArrayList<>(); list1.add(new Person()); List<User> list2 = new ArrayList<>(); list2.add(new User()); System.out.println("list1 = " + list1.getClass()); System.out.println("list2 = " + list2.getClass()); System.out.println(list1.getClass() == list2.getClass()); } }
运行结果:
显然,List<Person> 和 List<User> 在虚拟机中指向的类都是 ArrayList ,泛型信息被擦除了。
在泛型类被类型擦除的时候,之前泛型类中的类型参数部分如果没有指定上限,如 <T> 则会被转译成普通的 Object 类型,如果指定了上限如 <T extends String>则类型参数就被替换成类型上限 String。
类型擦除带来的局限性:
类型擦除,它会抹掉很多继承相关的特性,这是它带来的局限性。理解类型擦除有利于我们绕过开发当中可能遇到的雷区,同样理解类型擦除也能让我们绕过泛型本身的一些限制。
正常情况下,因为泛型的限制,编译器不让最后一行代码编译通过,因为类似不匹配。但是,基于对类型擦除的了解,利用反射,我们可以绕过这个限制。
public interface List<E> extends Collection<E>{ boolean add(E e); }
上面是 List 和其中的 add() 方法的源码定义。
因为 E 代表任意的类型,所以类型擦除时,add 方法其实等同于:
boolean add(Object obj);
那么,利用反射,我们绕过编译器去调用 add 方法
public class App { public static void main(String[] args) { List<Integer> list = new ArrayList<>(); list.add(123); try { Method method = list.getClass().getDeclaredMethod("add", Object.class); method.invoke(list,"abc"); method.invoke(list,55.5f); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } for (Object obj : list) { System.out.println(obj); } } }
运行结果是:
可以看到,根据类型擦除的原理,使用反射的手段就绕过了正常开发中编译器不允许的操作限制。
注意:
- 泛型类或泛型方法中,不接受8中基本数据类型;
- 需要使用他们的包装类;