Java 学习笔记
第九章 泛型
泛型入门
编译时不检查类型的异常
import java.util.ArrayList;
import java.util.List;
public class ListErr {
public static void main(String[] args)
{
List strList=new ArrayList();
strList.add("lancibe");
strList.add("xun");
strList.add(5);
strList.forEach(str -> System.out.println(((String)str).length()));
}
}
- 上面程序创建了一个LIst集合,而且只希望该集合保存字符串,但是如果不小心放入了一个Integer对象,则引发ClassCastExpection异常。
使用泛型
-
对于前面的程序,可以使用泛型来改进该程序:
import java.util.ArrayList; import java.util.List; public class GenericList { public static void main(String args[]) { //创建一个只想保存字符串的List集合 List<String> strList = new ArrayList<>(); strList.add("lancibe"); strList.add("xun"); //下面代码直接引起编译错误 strList.add(5); strList.forEach(str -> System.out.println(str.length())); } }
-
此外,在最后一行输出的时候,不需要进行强制类型转换,因为strList对象可以“记住”他的所有集合元素都是String类型。
Java 9 增强的“菱形”语法
-
在Java 7 以前,如果使用带有泛型的接口、类定义变量,那么调用构造器创建对象时构造器的后面也必须带泛型,这显得会有些多余。例如下面两条语句
List<String> strList = new ArrayList<String>(); Map<String, Integer> scores = new HashMap<String, Integer>();
-
上面两条语句后面的尖括号里的内容完全是多余的。故应该改写为如下格式
List<String> strList = new ArrayList<>(); Map<String, Integer> scores = new HashMap<>();
-
这种语法也被称为“菱形”语法,下面程序示范了Java 7 的菱形语法。
import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class DiamondTest { public static void main(String[] args) { //java自动推断出ArrayList的<>里应该是String List<String> books = new ArrayList<>(); books.add("lancibe"); books.add("xun"); //遍历 books.forEach(str -> System.out.println(str.length())); //使用map,结果一样 Map<String, List<String>> schoolsInfo = new HashMap<>(); List<String> schools = new ArrayList<>(); schools.add("123"); schools.add("456"); schoolsInfo.put("xx", schools); schoolsInfo.forEach((key, value) -> System.out.println(key+"-->"+value)); } }
-
程序运行结果如下:
lancibe@lancibe-PC:~/java/test/src/chapte9/src$ java DiamondTest 7 3 xx-->[123, 456]
-
Java 9 再次增强了“菱形”语法,他甚至允许在创建匿名内部类时使用菱形语法, Java可以根据上下文来推断匿名内部类中泛型的类型:
interface Foo<T> { void test(T t); } public class AnnoymousDiamond { public static void main(String[] args) { //制定Foo类中泛型为String Foo<String> f = new Foo<String>() { //必须要进行方法的重载,test方法的参数类型为String @Override public void test(String s) { System.out.println("test方法的s参数为:"+s); } }; //使用泛型通配符,此时相当于通配符的上限为Object Foo<?> fo = new Foo<Object>() { @Override public void test(Object o) { System.out.println("test方法的o参数为"+o); } }; //使用泛型通配符,此时通配符上限为Number Foo<? extends Number> fn = new Foo<Number>() { @Override public void test(Number number) { System.out.println("test方法的Number参数为:"+number); } }; } }
深入泛型
- 所谓泛型,就是允许在定义类、接口、方法时使用类型形参、这个类型形参(或叫泛型)将在声明变量、创建对象、调用方法时动态地指定(即传入实际的类型参数,也可称为类型实参)。Java 5 改写了集合框架中的全部接口和类,为这些接口、类增加了泛型支持,从而可以在声明集合变量、创建集合对象时传入类型实参,这就是前面程序中看到的List<String>和ArrayList<String>两种类型。
定义泛型接口、类
-
下面是Java 5 改写后List接口、Iterator接口、Map的代码片段。
public interface List<E> { void add<E x>; 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) ... }
-
可以为任何类、接口增加泛型声明(并不是只有集合类才可以使用,但是集合类确实是泛型使用的重要场所)。
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()); //同理 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类时省略泛型的形式被称为原始类型(raw type)
-
如果从Apple<String>类派生子类,则在Apple类中所有使用T类型的地方都将被替换成String类型,即他的子类将会继承到String getInfo()和void setInfo(String info)两个方法,如果子类需要重写父类的方法,就必须注意到这点。
public class A1 extends Apple<String>{ @Override public String getInfo() { return "子类"+super.getInfo(); } }
-
如果使用Apple类时没有传入实际的类型(即使用原始类型),Java编译器可能会发出警告:使用了未经检查或不安全的操作——这就是泛型检查的警告。如果希望看到该警告提示的更详细的信息,则可以通过javac命令增加
-Xlint:unchecked
选项来实现。此时,系统会把Apple<T>类里的T形参当成Object来处理。public class A2 extends Apple { @Override public String getInfo() { //super.getIndo()是Object类型 //所以加toString才是String类型 return super.getInfo().toString(); } }
并不存在泛型类
List<String> l1 = new ArrayList<>();
List<Integer> l2 = new ArrayList<>();
System.out.println(l1.getClass() == l2.getClass());
- 上面的代码片段输出true
类型通配符
-
正如前面的,当使用一个泛型类时(包括声明变量和创建对象两种情况),都应该为这个泛型类传入一个类型实参。如果没有传入类型实际参数,编译器就会提出泛型警告。假设现在需要定义一个方法,该方法里有一个集合形参,集合形参的元素类型是不确定的,那应该怎样定义呢。
public void test(List c) { for (int i = 0 ; i < c.size() ; i++) { System.out.println(c.get(i)); } }
-
上面的代码没有问题,但是List接口是需要类型参数的,因此会引起泛型警告。应使用下面方法
public void test(List<Object> c) { for (int i = 0 ; i < c.size() ; i++) { System.out.println(c.get(i)); } }
-
与数组进行对比,先看数组是如何工作的,在数组中,程序可以直接把一个Integer[]数组赋给一个Number[]变量。如果试图把一个Double对象保存在该Number[]数组中,编译可以通过,但是在运行时跑出ArrayStoreException异常。
public class ArrayErr { public static void main(String[] args) { Integer[] ia = new Integer[5]; Number[] na = ia; na[0] = 0.5; } }
-
上面程序会在运行时异常。
使用类型通配符
-
为了表示各种泛型List的父类,可以使用类型通配符,类型通配符是一个问号,表示将一个问号作为类型实参传给List集合,写作:List<?>(意思是元素类型未知的List)。这个问号被称为通配符,他的元素类型可以匹配任何类型。可以将上面方法改写为如下格式:
public void test(List<?> c) { for (int i = 0 ; i < c.size() ; i++) { System.out.println(c.get(i)); } }
-
现在任何类型的List来调用它程序依然可以访问c中的元素,其类型是Object,这永远是安全的,因为不管List的真是类型是什么,它包含的都是Object
-
但这种带通配符的List仅表示他是各种泛型List的父类,并不能把元素加入其中,否则代码将会引起错误。
List<?> c = new ArrayList<String>(); //下面代码将引起编译错误 c.add(new Object());
设定类型通配符的上限
-
下面先定义三个形状类
import java.awt.*; public abstract class Shape { public abstract void draw(Canvas c); }
import java.awt.*; public class Circle extends Shape{ //方法实现 @Override public void draw(Canvas c) { System.out.println("在画布"+c+"上画一个圆"); } }
import java.awt.*; public class Rectangle extends Shape { @Override public void draw(Canvas c) { System.out.println("把一个矩形画在画布"+c+"上"); } }
-
Shape是一个抽象父类,有两个子类:Circle和Rectangle。接下来定义一个Canvas类,该画布类可以画数量不等的形状(Shape子类的对象),那应该如何定义这个Canvas类呢。考虑如下的Canvas实现类
import java.util.List; public class Canvas { //同时在画布上绘制多个形状 public void drawAll(List<Shape> shapes) { for (Shape s : shapes) { s.draw(this); } } }
-
这将会引起编译错误。关键在于List<Circle>并不是List<Shape>的子类型,所以不能把前者对象当成后者使用。被限制的泛型通配符如下:
List<? extends Shape>
-
有了这种被限制的泛型通配符,就可以把上面的Canvas程序改为如下形式
import org.jetbrains.annotations.NotNull; import java.util.List; public class Canvas { //同时在画布上绘制多个形状 public void drawAll(@NotNull List<? extends Shape> shapes) { for (Shape s : shapes) { s.draw(this); } } }
-
对于更广泛的泛型来说,指定通配符上限就是为了支持类型形变。比如Foo是Bar的子类,A<Bar>就相当于A<? extends Foo>的子类,可以将A<Bar>赋值给A<? extends Foo>类型的变量,这种型变方式被称为协变。
-
对于协变的泛型类来说,它只能调用泛型类型作为返回值类型的方法(编译器会将该方法返回值当成通配符上限的类型),而不能调用泛型类型作为参数的方法。
设定类型通配符的下限
-
形式为A<? super Xxx>
import java.util.ArrayList; import java.util.Collection; import java.util.List; public class MyUtils { //下面dest集合元素的类型必须与src集合元素的类型相同,或者是其父类 public static <T> T copy(Collection<? super T> dest, Collection<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); } }
-
使用这种语句,就可以保证在最后调用后推断出最后一个被复制的元素类型是Integer,而不是笼统的Number类型。
-
实际上,Java 集合框架中的TreeSet<E>有一个构造器也用到了这种设定通配符下限的语法,如下所示。
TreeSet(Comparator<? super E> c)
-
正如前一章介绍的,TreeSet会对集合中的元素按自然顺序或定制顺序进行排序。如果需要TreeSet对集合中的所有元素进行定制排序,则要求TreeSet对象有一个与之关联的Comparator对象。上面构造器中的参数c就是进行定制排序的Comparator对象。
-
Comparator接口也是一个带泛型声明的接口
public interface Comparator<T> { int compare (T fst, T snd); }
-
通过这种带下限的通配符的语法,可以在创建TreeSet对象时灵活地选择合适的Comparator。假定需要创建一个TreeSet<String>集合,并传入一个可以比较String大小的Comparator,这个Comparator既可以是Comparator<String>,也可以是Comparator<Object>——只要尖括号里传入的类型是String的父类型(或它本身)即可。
import java.util.Comparator; import java.util.TreeSet; public class TreeSetTest { public static void main(String[] args) { //Comparator的实际类型时TreeSet的元素类型的父类,满足要求 TreeSet<String> ts1 = new TreeSet<>(new Comparator<Object>() { @Override public int compare(Object fst, Object snd) { return hashCode() > snd.hashCode() ? 1 : hashCode() < snd.hashCode() ? -1 : 0; } }); ts1.add("hello"); ts1.add("wa"); TreeSet<String> ts2 = new TreeSet<>(new Comparator<String>() { @Override public int compare(String o1, String o2) { return o1.length() > o2.length() ? -1 : o1.length() < o2.length() ? 1 : 0; } }); ts2.add("hello"); ts2.add("wa"); System.out.println(ts1); System.out.println(ts2); } }
设定泛型形参的上限
-
Java 泛型不仅允许在使用通配符形参时设定上限,而且可以在定义泛型形参时设定上限,用于表示传给该泛型形参的实际类型要么是该上限类型,要么是该上限类型的子类。
public class Apple2<T extends Number> { T col; public static void main(String[] args) { Apple2<Integer> ai = new Apple2<>(); Apple2<Double> ad = new Apple2<>(); //下面代码编译异常,试图把String类型传给T形参 Apple2<String> as = new Apple2<>(); } }
泛型方法
定义泛型方法
-
假设需要实现这样的方法:该方法负责将一个Object数组的所有元素添加到一个Collection集合中。考虑采用如下代码实现:
static void fromArrayToCollection(Object[] a, Collection<Object> c) { for (Object o : a) { c.add(o); } }
-
上面定义的方法没有任何问题,关键在于方法中的c形参,他的数据类型是Collection<Object>。正如前面介绍的,Collection<String>不是Collection<Object>的子类型——所以这个方法的功能非常有限,他只能将Object[]数组的元素赋值到元素为Object的Collection集合中,即下面代码编译错误
String[] strArr = { "a", "b"}; List<String> strList = new ArrayList<>(); fromArrayToCollection(strArr, strList);
-
为解决这个问题,可以使用Java 5 提供的泛型方法(Generic Method)。所谓泛型方法,就是在声明方法时定义一个或多个泛型形参。泛型方法的语法格式如下:
修饰符 <T, S> 返回值类型 方法名(形参列表) { //方法体 }
-
前面的fromArrayToCollection可以改写为如下格式:
static <T> void fromArrayToCollection (T[] a, Collection<T> c) { for (T o : a) { c.add(o); } }
-
下面程序示范了完整的用法:
import java.util.ArrayList; import java.util.Collection; public class GenericMethodTest { 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); } }
泛型方法和类型通配符的区别
- 大多数时候都可以使用泛型方法来代替类型通配符,也可以同时使用泛型方法和通配符
Java 7 的“菱形”语法与泛型构造器
-
一旦定义了泛型构造器,接下来在调用构造器时,就不仅可以让Java根据数据参数的类型来“推断”泛型形参的类型,而且程序员也可以显式地为构造器中的泛型形参指定实际的类型:
class Foo2 { public <T> Foo2(T t) { System.out.println(t); } } public class GenericConstructor { public static void main(String[] args) { new Foo2("lancibe"); new Foo2(200); new <String> Foo2("xun"); new <Double> Foo2(12.3); } }
-
要注意:**如果程序显式地指定了泛型构造器中声明的泛型形参的实际类型,则不可以使用“菱形”语法。**例如如下代码就是错误的
MyClas<String> mc = new <Integer> MyClass<>(5);
泛型方法与方法重载
-
形式如下:
public static <T> void copy(Collection<T> dest, Collection<? extends T> src){ ...} public static <T> void copy(Collection<? super T? dest, Collection<T> src){ ...}
-
比较好理解。
Java 8 改进的类型推断
-
Java 8 改进了泛型方法的类型推断能力,类型推断主要有如下两个方面
- 可通过调用方法的上下文来推断泛型的目标类型
- 可在方法调用链中,将推断得到的泛型传递到最后一个方法
-
如下程序示范了Java 8 对泛型方法的类型推断
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()); //无需使用下面语句 MyUtil.cons(42, MyUtil.<Integer>nil()); } }
擦除和转换
-
在严格的泛型代码里,带泛型声明的类总应该带着类型参数。但为了与老的Java代码保持一致,也允许在使用带泛型声明的类时不指定实际类型。如果没有为这个泛型类指定实际的类型,此时被称作raw type(原始类型),默认是声明该泛型形参时指定的第一个上限类型。
-
当把一个具有泛型信息的对象赋给另一个没有泛型信息的变量时,所有在尖括号之间的类型信息都被扔掉。比如一个List<String>被转换为List,则该List对集合元素的类型检查变成了泛型参数上限(即Object)。下面程序示范了这种擦除:
class Apple3<T extends Number> { T size; public Apple3(){ } public Apple3(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) { Apple3<Integer> a = new Apple3<>(6); //a的getSize()方法返回Integer对象 Integer as = a.getSize(); //把a对象赋给Apple3变量,丢失尖括号里的类型信息 Apple3 b = a; //b只知道size的类型是Number Number size1 = b.getSize(); //下面代码编译错误 //Integer size2 = b.getSize(); } }
泛型与数组
-
Java泛型有一个很重要的设计原则——如果一段代码在编译时没有提出未经检查的转换的警告,则运行时不会引发ClassCastException异常。正是基于这个原因,数组元素的类型不能包含泛型变量或泛型形参,除非是无上限的类型通配符。
-
Java允许创建无上限的通配符泛型数组,例如
new ArrayList<?>[10]