Java泛型总结
泛型使用
从Java5开始引入了“参数化类型”的概念,允许在创建集合的时候指定集合元素的类型
public class GenericList {
public static void main(String[] args)
{
// 创建一个只想保存字符串的List集合
List<String> strList = new ArrayList<String>(); // ①
strList.add("Java");
strList.add("Android");
// 下面代码将引起编译错误
// strList.add(5); // ②
strList.forEach(str -> System.out.println(str.length())); // ③
}
}
Java9增强的“菱形”语法
Java7之前,如果使用带泛型接口、类定义变量,那么调用构造函数创建对象后面也必须带泛型
如:
List<String> list = new ArrayList<String>();
从Java7开始就不再需要了,只要给出一对尖括号就可以了
public class DiamondTest {
public static void main(String[] args)
{
// Java自动推断出ArrayList的<>里应该是String
List<String> books = new ArrayList<>();
books.add("Java");
books.add("Android");
// 遍历books集合,集合元素就是String类型
books.forEach(ele -> System.out.println(ele.length()));
// Java自动推断出HashMap的<>里应该是String , List<String>
Map<String , List<String>> schoolsInfo = new HashMap<>();
// Java自动推断出ArrayList的<>里应该是String
List<String> schools = new ArrayList<>();
schools.add("斜月三星洞");
schools.add("西天取经路");
schoolsInfo.put("孙悟空" , schools);
// 遍历Map时,Map的key是String类型,value是List<String>类型
schoolsInfo.forEach((key , value) -> System.out.println(key + "-->" + value));
}
}
深入
泛型就是在定义类、接口、方法时使用类型参数,这个类型参数将在声明变量,创建对象、调用方法时动态指定
泛型接口、类
如:
public interface List<E>
{
// 接口中,E可以作为类型使用
// 下面方法可以使用E作为参数类型
void add(E e);
Iterator<E> iterator();
}
public interface Iterator<E>
{
E next();
boolean hasNext();
}
// 指定两个泛型形参
public interface Map<K,V>
{
Set<K> keySet();
V put(K key,V value);
}
Set<K>形式是一种特殊的数据类型,是一种与Set不同的数据类型---可以认为是Set类型的子类
可以为任何类、接口增加泛型声明
public class Apple<T> {
// 使用T类型定义实例变量
private T info;
public Apple(){}
// 下面方法中使用T类型来定义构造器
public Apple(T info)
{
this.info = info;
}
public void setInfo(T info)
{
this.info = info;
}
public T getInfo()
{
return this.info;
}
public static void main(String[] args)
{
// 由于传给T形参的是String,所以构造器参数只能是String
Apple<String> a1 = new Apple<>("苹果");
System.out.println(a1.getInfo());
// 由于传给T形参的是Double,所以构造器参数只能是Double或double
Apple<Double> a2 = new Apple<>(5.67);
System.out.println(a2.getInfo());
}
}
泛型类派生子类
当想要实现带泛型声明的接口或者继承带泛型声明的父类时,这些接口,父类不能再包含泛型形参
这个就是错误的:
public class A extends Apple<T>
如果想从Apple类派生一个子类,则可以这样:
public class A extends Apple<String>
不为泛型形参传入实际类型参数也是可以的:
public class A extends Apple
这种使用Apple类时省略泛型的形式被称为原始类型
子类重写父类方法需要主要返回类型
public class A1 extends Apple<String>{
// 正确重写了父类的方法,返回值
// 与父类Apple<String>的返回值完全相同
public String getInfo()
{
return "子类" + super.getInfo();
}
/*
// 下面方法是错误的,重写父类方法时返回值类型不一致
public Object getInfo()
{
return "子类";
}
*/
}
如果没有传入实际类型,那么系统会把Apple<T>中T形参当初Object类型处理
//Raw use of parameterized class 'Apple'
public class A2 extends Apple{
// 重写父类的方法
public String getInfo()
{
// super.getInfo()方法返回值是Object类型,
// 所以加toString()才返回String类型
return super.getInfo().toString();
}
}
不存在泛型类
不管泛型的实际类型参数是什么,它们在运行时总有同样的类
不管为泛型形参传入哪一种类型实参,它们依然被当成同一个类处理
因此在静态方法,静态初始化块,静态变量的声明和初始化中不允许使用泛型形参
public class R<T> {
// 下面代码错误,不能在静态变量声明中使用泛型形参
// static T info;
T age;
public void foo(T msg){}
// 下面代码错误,不能在静态方法声明中使用泛型形参
// public static void bar(T msg){}
}
类型通配符
考虑这段代码:
// Raw use of parameterized class 'List'
public void test(List list){
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}
没有给List传入实例类型参数,将引起警告
那么更改下:
public void test(List<Object> list){
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}
声明没有问题,但真正传入的实际参数值可能不符合我们的预期
List<String> strings = new ArrayList<>();
test(strings);
结果报错,说明List<String>对象不能当做List<Object>使用,也就是说List<String>类并不是List<Object>类的子类
细节:
如果F是B的一个子类型(子类或者子接口),而G是具有泛型声明的类或接口,G<F>并不是G<B>的子类型
如:
public class ArrayErr {
public static void main(String[] args)
{
// 定义一个Integer数组
Integer[] ia = new Integer[5];
// 可以把一个Integer[]数组赋给Number[]变量
Number[] na = ia;
// 下面代码编译正常,但运行时会引发ArrayStoreException异常
// 因为0.5并不是Integer
na[0] = 0.5; // ①
List<Integer> iList = new ArrayList<>();
// 下面代码导致编译错误
// List<Number> nList = iList;
}
}
数组和泛型不同,假设F是B的一个子类型(子类或者子接口),那么F[] 依然是B[]的子类型,G<F>并不是G<B>,F[ ]自动向上转型为B[]的方式叫做型变
类型通配符
类型通配符是一个问号?,将一个问号作为类型实参传递给List集合,写作:List<?>(意思是元素类型未知的List),这个问号?就是通配符,它的元素类型可以匹配任何类型
但这种带通配符的List仅仅表示它是各种泛型List的父类,并不能把元素加入到其中
List<?> list = new ArrayList<String>();
list.add(new Object());
程序无法确定list中元素类型,所以不能添加对象
设定通配符的上限
如果不希望List<?> 是任何泛型List的父类,只希望它代表某一类泛型List的父类
例子:
// 定义一个抽象类Shape
public abstract class Shape
{
public abstract void draw(Canvas c);
}
// 定义Shape的子类Circle
public class Circle extends Shape
{
// 实现画图方法,以打印字符串来模拟画图方法实现
public void draw(Canvas c)
{
System.out.println("在画布" + c + "上画一个圆");
}
}
// 定义Shape的子类Rectangle
public class Rectangle extends Shape
{
// 实现画图方法,以打印字符串来模拟画图方法实现
public void draw(Canvas c)
{
System.out.println("把一个矩形画在画布" + c + "上");
}
}
public class Canvas {
// 同时在画布上绘制多个形状
public void drawAll_error(List<Shape> shapes)
{
for (Shape s : shapes)
{
s.draw(this);
}
}
// 同时在画布上绘制多个形状,使用被限制的泛型通配符
public void drawAll(List<? extends Shape> shapes)
{
for (Shape s : shapes)
{
s.draw(this);
}
}
public static void main(String[] args)
{
List<Circle> circleList = new ArrayList<Circle>();
Canvas c = new Canvas();
// 由于List<Circle>并不是List<Shape>的子类型,
// 所以下面代码引发编译错误
// c.drawAll_error(circleList);
c.drawAll(circleList);
}
}
List<? extends Shape>表示泛型形参必须是Shape子类的List
List<? extends Shape>是受限通配符的例子,问号?代表一个未知的类型,但此处的这个未知类型一定是Shape的子类型(Shape本身也可以),因此可以把Shape称为这个通配符的上限
假设F是B的子类,那么A<B>就相当于A<? extends F>的子类,可以将A<B>赋值给A<? extends F>类型的变量,这种型变方式称之为协变
指定通配符上限的集合,只能从集合中取出元素(取出的元素总是上限的类型),不能向集合中添加元素(因为编译器无法确定集合元素实际上是哪种子类型)
设定通配符的下限
通配符下限用<? super 类型>的方式来指定,指定下限就是为了支持类型型变
F是B的子类,当需要一个A<? super B>变量时,可以将A<F>,A<Object>赋值给A<? suoer B>类型的变量,这种型变称之为逆变
对于逆变的泛型集合,编译器只知道集合元素是下限的父类型,但具体是哪种父类型是不确定的,因此这种逆变的泛型集合能向其中添加元素,从集合取出元素只能当成Object处理
例子:
public class MyUtils {
// 下面dest集合元素类型必须与src集合元素类型相同,或是其父类
public static <T> T copy(List<? super T> dest
, List<T> src)
{
T last = null;
for (T ele : src)
{
last = ele;
// 逆变的泛型集合添加元素是安全的
dest.add(ele);
}
return last;
}
public static void main(String[] args)
{
List<Number> ln = new ArrayList<>();
List<Integer> li = new ArrayList<>();
li.add(5);
// 此处可准确的知道最后一个被复制的元素是Integer类型
// 与src集合元素的类型相同
Integer last = copy(ln , li); // ①
System.out.println(ln);
}
}
Java集合框架中的例子:
public class TreeSetTest {
public static void main(String[] args)
{
// Comparator的实际类型是TreeSet的元素类型的父类,满足要求
TreeSet<String> ts1 = new TreeSet<>(
new Comparator<Object>()
{
public int compare(Object fst, Object snd)
{
return hashCode() > snd.hashCode() ? 1
: hashCode() < snd.hashCode() ? -1 : 0;
}
});
ts1.add("hello");
ts1.add("wa");
// Comparator的实际类型是TreeSet元素的类型,满足要求
TreeSet<String> ts2 = new TreeSet<>(
new Comparator<String>()
{
public int compare(String first, String second)
{
return first.length() > second.length() ? -1
: first.length() < second.length() ? 1 : 0;
}
});
ts2.add("hello");
ts2.add("wa");
System.out.println(ts1);
System.out.println(ts2);
}
}
泛型形参的上限
Java可以在定义泛型形参时设定上限,用于表示传给该泛型形参的实际类型要么是该上限类型,要么是该上限类型的子类
public class Apple<T extends Number>
{
T col;
public static void main(String[] args)
{
Apple<Integer> ai = new Apple<>();
Apple<Double> ad = new Apple<>();
// 下面代码将引起编译异常,下面代码试图把String类型传给T形参
// 但String不是Number的子类型,所以引发编译错误
// Apple<String> as = new Apple<>(); // ①
}
}
Apple<T extends Number>限定了为T传入的实际参数类型只能是Number或其子类
甚至可以为泛型形参设定多个上限(至多一个父类上限,可以有多个接口上限)
public class T2<T extends Number & java.io.Serializable> {
}
接口上限必须位于类上限后面
泛型方法
定义泛型方法
实现一个方法,把一个Object数组的所有元素添加到一个Collection集合
首先想到:
public static void the_ArrayToCollection(Object[] objects, Collection<Object> collection){
for (Object o:objects){
collection.add(o);
}
}
定义没有问题,但是Collection<String>不是Collection<Object>的子类型,所以这个方法功能有限,只可以把Object数组元素复制到元素为Object的collection集合
下面代码会出错:
String[] strings = {"sdsd","dff"};
List<String> list = new ArrayList<>();
// the_ArrayToCollection(strings,list);
使用通配符Collection<?>也不行
这个时候可以考虑泛型方法,语法格式:
修饰符 <T,S> 返回值类型 方法名字(参数列表)
与普通方法比较,泛型方法多了泛型形参声明
泛型形参声明用尖括号括起来,多个泛型形参之间用逗号隔开,所有的泛型形参声明放在方法修饰符和方法返回值类型之间
把上面例子改成:
static <T> void the_ArrayToCollection(T[] a,Collection<T> c){
for (T t:a){
c.add(t);
}
}
完整用法:
public class GenericMethodTest {
// 声明一个泛型方法,该泛型方法中带一个T泛型形参,
static <T> void fromArrayToCollection(T[] a, Collection<T> c)
{
for (T o : a)
{
c.add(o);
}
}
public static void main(String[] args)
{
Object[] oa = new Object[100];
Collection<Object> co = new ArrayList<>();
// 下面代码中T代表Object类型
fromArrayToCollection(oa, co);
String[] sa = new String[100];
Collection<String> cs = new ArrayList<>();
// 下面代码中T代表String类型
fromArrayToCollection(sa, cs);
// 下面代码中T代表Object类型
fromArrayToCollection(sa, co);
Integer[] ia = new Integer[100];
Float[] fa = new Float[100];
Number[] na = new Number[100];
Collection<Number> cn = new ArrayList<>();
// 下面代码中T代表Number类型
fromArrayToCollection(ia, cn);
// 下面代码中T代表Number类型
fromArrayToCollection(fa, cn);
// 下面代码中T代表Number类型
fromArrayToCollection(na, cn);
// 下面代码中T代表Object类型
fromArrayToCollection(na, co);
// 下面代码中T代表String类型,但na是一个Number数组,
// 因为Number既不是String类型,
// 也不是它的子类,所以出现编译错误
// fromArrayToCollection(na, cs);
}
}
方法声明中定义的泛型只能在该方法中使用,接口、类声明中定义的泛型可以在整个接口、类中使用
以下例子可能出现的错误:
public class ErrorTest {
// 声明一个泛型方法,该泛型方法中带一个T泛型形参
static <T> void test(Collection<T> from, Collection<T> to)
{
for (T ele : from)
{
to.add(ele);
}
}
public static void main(String[] args)
{
List<Object> as = new ArrayList<>();
List<String> ao = new ArrayList<>();
// 下面代码将产生编译错误
// test(as , ao);
}
}
test方法想要将前一个集合里面的元素复制到下一个集合里面
方法中两个形参都是Collection<T>类型,这就要求使用时候集合中实参中的泛型类型一样
为了避免,可以:
public class RightTest {
// 声明一个泛型方法,该泛型方法中带一个T形参
static <T> void test(Collection<? extends T> from , Collection<T> to)
{
for (T ele : from)
{
to.add(ele);
}
}
public static void main(String[] args)
{
List<Object> ao = new ArrayList<>();
List<String> as = new ArrayList<>();
// 下面代码完全正常
test(as , ao);
}
}
"菱形"语法以及泛型构造函数
Java允许在构造函数中声明泛型形参。这就是所谓的泛型构造函数
例子:
class Foo
{
public <T> Foo(T t)
{
System.out.println(t);
}
}
public class GenericConstructor
{
public static void main(String[] args)
{
// 泛型构造器中的T类型为String。
new Foo("Java");
// 泛型构造器中的T类型为Integer。
new Foo(200);
// 显式指定泛型构造器中的T类型为String,
// 传给Foo构造器的实参也是String对象,完全正确。
new <String> Foo("Android");
// 显式指定泛型构造器中的T类型为String,
// 但传给Foo构造器的实参是Double对象,下面代码出错
// new <String> Foo(12.3);
}
}
"菱形"语法允许调用构造函数时在构造函数后使用一对尖括号来代表泛型信息,如果显式指定了泛型构造函数中声明的泛型形参的实际类型,就不可以使用"菱形"语法
class MyClass<E>
{
public <T> MyClass(T t)
{
System.out.println("t参数的值为:" + t);
}
}
public class GenericDiamondTest
{
public static void main(String[] args)
{
// MyClass类声明中的E形参是String类型。
// 泛型构造器中声明的T形参是Integer类型
MyClass<String> mc1 = new MyClass<>(5);
// 显式指定泛型构造器中声明的T形参是Integer类型,
MyClass<String> mc2 = new <Integer> MyClass<String>(5);
// MyClass类声明中的E形参是String类型。
// 如果显式指定泛型构造器中声明的T形参是Integer类型
// 此时就不能使用"菱形"语法,下面代码是错的。
// MyClass<String> mc3 = new <Integer> MyClass<>(5);
}
}
Java8改进的类型推断
-
可以通过调用方法的上下文来推断泛型的目标类型
-
可以在方法调用链中,将推断得到的泛型传递到最后一个方法
class MyUtil<E>
{
public static <Z> MyUtil<Z> nil()
{
return null;
}
public static <Z> MyUtil<Z> cons(Z head, MyUtil<Z> tail)
{
return null;
}
E head()
{
return null;
}
}
public class InferenceTest
{
public static void main(String[] args)
{
// 可以通过方法赋值的目标参数来推断类型参数为String
MyUtil<String> ls = MyUtil.nil();
// 无需使用下面语句在调用nil()方法时指定类型参数的类型
MyUtil<String> mu = MyUtil.<String>nil();
// 可调用cons方法所需的参数类型来推断类型参数为Integer
MyUtil.cons(42, MyUtil.nil());
// 无需使用下面语句在调用nil()方法时指定类型参数的类型
MyUtil.cons(42, MyUtil.<Integer>nil());
// 希望系统能推断出调用nil()方法类型参数为String类型,
// 但实际上Java 8依然推断不出来,所以下面代码报错
// String s = MyUtil.nil().head();
String s = MyUtil.<String>nil().head();
}
}
擦除与转换
当把一个具有泛型信息的对象赋值给另一个没有泛型信息的变量时,所有在尖括号之间的类型信息都将被扔掉
class Apple<T extends Number>
{
T size;
public Apple()
{
}
public Apple(T size)
{
this.size = size;
}
public void setSize(T size)
{
this.size = size;
}
public T getSize()
{
return this.size;
}
}
public class ErasureTest
{
public static void main(String[] args)
{
Apple<Integer> a = new Apple<>(6); // ①
// a的getSize方法返回Integer对象
Integer as = a.getSize();
// 把a对象赋给Apple变量,丢失尖括号里的类型信息
Apple b = a; // ②
// b只知道size的类型是Number
Number size1 = b.getSize();
// 下面代码引起编译错误
// Integer size2 = b.getSize(); // ③
}
}
对泛型而言,可以直接把一个List对象赋值给一个List<String>对象,编译器仅仅提示“未经检查的转换”
public class ErasureTest2 {
public static void main(String[] args)
{
List<Integer> li = new ArrayList<>();
li.add(6);
li.add(9);
List list = li;
// 下面代码引起“未经检查的转换”的警告,编译、运行时完全正常
List<String> ls = list; // ①
// Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
// 但只要访问ls里的元素,如下面代码将引起运行时异常。
System.out.println(ls.get(0));
}
}
执行List list = li后,编译器就失去了li的泛型信息,这是典型的擦除
而对list变量实际上引用的是List<Integer>集合,所以试图把集合里面元素当String取出来就会报错
泛型数组
数组元素的类型不能包含泛型变量或者泛型形参,除非是无上限的类型通配符,但是可以声明元素类型包含泛型变量或泛型形参的数组
也就是只能声明List<String>[]形式的数组,但不能创建ArrayList<String>[ 10 ]这样的数组对象
例子:
public class GenericAndArray {
public static void main(String[] args)
{
// // 下面代码编译时有“[unchecked] 未经检查的转换”警告
// List<String>[] lsa = new ArrayList[10];
// // 将lsa向上转型为Object[]类型的变量
// Object[] oa = lsa;
// List<Integer> li = new ArrayList<>();
// li.add(3);
// oa[1] = li;
// // 下面代码引起ClassCastException异常
// String s = lsa[1].get(0); // ①
List<?>[] lsa = new ArrayList<?>[10];
Object[] oa = lsa;
List<Integer> li = new ArrayList<>();
li.add(3);
oa[1] = li;
Object target = lsa[1].get(0);
if (target instanceof String)
{
// 下面代码安全了
String s = (String) target;
}
}
}
网友评论